在 TCP 套接字程序中,客户端发送一些数据,但服务器需要多次读取。为啥?

Posted

技术标签:

【中文标题】在 TCP 套接字程序中,客户端发送一些数据,但服务器需要多次读取。为啥?【英文标题】:In TCP socket program,client send some data, but server need read multiple times. why?在 TCP 套接字程序中,客户端发送一些数据,但服务器需要多次读取。为什么? 【发布时间】:2016-09-05 08:12:24 【问题描述】:

我有一个关于套接字的问题。我从客户端向服务器发送 N 大小的数据,N 大小小于 100 字节。所以我认为我的数据不应该拆分为多个 tcp 数据包。在我看来,客户端发送数据应该一次做,服务器一次可以接收数据。但是结果并不理想。实际情况是服务器需要调用读取数据。我不明白。按照代码:

epoll_server.cpp(只接收数据)

#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <netdb.h>

#define BUFSIZE 1024
#define INITSIZE 1024
#define MAXEVENTCOUNT 10240

// add non-blocking to sockfd
int make_socket_non_blocking(int fd)

    // get initial flag
    int src_flags;
    src_flags= fcntl(fd, F_GETFL,0);           
    if(src_flags == -1)
                        
        perror("fcntl get error.");
        return-1;
    

    // add non-blocking
    int new_flags = src_flags | O_NONBLOCK;
    int ret_value;
    ret_value = fcntl(fd, F_SETFL, new_flags);
    if(ret_value == -1)
    
        perror("fcntl set error.");
        return-1;
    

    return 0;



// main function
int main(int argc, char* argv[])

    int server_sockfd, client_sockfd;
    int server_len;
    struct sockaddr_in server_address;

    // create server socket fd
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // init server address struct
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(9567);
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_len = sizeof(server_address);

    // bind server address info for server fd
    if((bind(server_sockfd, (struct sockaddr*)&server_address, server_len)) == -1)
    
        perror("bind error");
        exit(EXIT_FAILURE);
    

    // let server is listened state
    listen(server_sockfd, 5);
    printf("server start waiting for connect...\r\n");

    // only suggestion
    int efd = epoll_create(INITSIZE);
    if(-1 == efd)
    
        printf("epoll_create error happen.\n");
        return -1;
    

    // set server_sockfd
    struct epoll_event server_event, event;
    server_event.data.fd = server_sockfd;
    server_event.events = EPOLLIN | EPOLLET;
    int ret_epollctl = epoll_ctl(efd, EPOLL_CTL_ADD, server_sockfd, &server_event);
    if(-1 == ret_epollctl)
    
        printf("epoll_ctl error happen when efd is adding server_sockfd.\n");
        return -1;
    

    /* event loop */
    struct epoll_event* return_events;
    // set timeout is 3000 ms
    int timeout_msecond = 3000;    
    return_events = (struct epoll_event*)malloc(MAXEVENTCOUNT*sizeof(struct epoll_event));
    int count = 0;
    while(1)
    
        int ret_epollwait = epoll_wait(efd, return_events, MAXEVENTCOUNT, timeout_msecond);
        // part_1:epoll_wait error happen
        if(-1 == ret_epollwait)
        
            printf("logged epoll_wait error happen.\n");
            continue;
        
        // part_2:epoll_wait timeout
        if(0 == ret_epollwait)
        
            printf("logged epoll_wait timeout.\n");
            continue;
        
        // part_3:do some other event
        int index = 0;
        for(index = 0; index < MAXEVENTCOUNT; index++)
        
            // part_3-1:hup ...
            if((return_events[index].events & EPOLLERR)
             || (return_events[index].events & EPOLLHUP)
             || !(return_events[index].events & EPOLLIN) )
                            
                continue;
            

            // part_3-2:is connection
            if(return_events[index].data.fd == server_sockfd)
            
                struct sockaddr_in client_address;
                int client_len = sizeof(client_address);
                // server accept connection from client
                int client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_address, (socklen_t*)&client_len);
                // part_3-2-1:connection error happen
                if(-1 == client_sockfd)
                
                    if((EAGAIN == errno)
                     || (EWOULDBLOCK == errno) )
                                            
                        continue;
                    
                    else
                    
                        printf("accept error occured.\n");
                        continue;
                    
                
                else // part_3-2-2:normal connection
                
                    // get clinet some information
                    char hostinfo_buf[BUFSIZE] = 0;
                    char servname_buf[BUFSIZE] = 0;
                    int tmp_ret = getnameinfo((struct sockaddr*)&client_address, client_len, hostinfo_buf, sizeof(hostinfo_buf), servname_buf, sizeof(servname_buf), NI_NUMERICHOST| NI_NUMERICSERV);
                    if(0 == tmp_ret)
                    
                        printf("Accepted connection on descriptor %d:ip=%s, port=%s.\n", client_sockfd, hostinfo_buf, servname_buf);
                    
                    // set client_sockfd to non-blocking
                    tmp_ret = make_socket_non_blocking(client_sockfd);
                    if(-1 == tmp_ret)
                    
                        printf("set client_sockfd=%d to non-blocking error occured.\n", client_sockfd);
                        abort();
                    

                    // set client_sockfd is EPOLLIN, EPOLLET
                    event.data.fd = client_sockfd;
                    event.events = EPOLLIN | EPOLLET;
                    tmp_ret = epoll_ctl(efd, EPOLL_CTL_ADD, client_sockfd, &event);
                    if(tmp_ret == -1)
                    
                        printf("efd add %d has a error.\n", client_sockfd);
                        continue;
                    
                    printf("add descriptor %d:ip=%s, port=%s successfully.\n", client_sockfd, hostinfo_buf, servname_buf);
                
                continue;
            

            // part_3-3:read data from client            
            printf("read data start++++\n");
            int temp = 0;

            // get recv_cache size start
            int recvsize = 0;
            socklen_t optlen = sizeof(recvsize);
            int err = getsockopt(return_events[index].data.fd, SOL_SOCKET, SO_RCVBUF, &recvsize, &optlen);
            printf("recv cache size :%d\n", recvsize);
            // get recv_cache size end

            while(1) // start while(1)
            
                printf("%d times read data\n", ++temp);
                char* recv_buffer = (char*)malloc(1024+1);
                memset(recv_buffer, 0, 1025);
                // int ret_read = read(return_events[index].data.fd, recv_buffer, sizeof(recv_buffer));
                int ret_read = recv(return_events[index].data.fd, recv_buffer, sizeof(recv_buffer), 0);
                // part_3-3-1:read return error
                if(-1 == ret_read)
                
                    if(EAGAIN != errno)
                    
                        printf("read data from %d error occured, errno=%d, %s.\n", return_events[index].data.fd, errno, strerror(errno));
                    
                    break;
                
                // part_3-3-2:no data
                if(0 == ret_read)
                
                    continue;
                
                // part_3-3-3:output data. If data is 'bye', connection will close.
                if(ret_read > 0)
                
                    printf("%d client's data:size=%dbyte, content=%s\n", return_events[index].data.fd, ret_read, recv_buffer);
                    // part_3-3-3-1:close connection and remove client_sockfd
                    if((recv_buffer[0] == 'b') 
                    && (recv_buffer[1] == 'y')
                    && (recv_buffer[2] == 'e') )
                    

                        close(return_events[index].data.fd);
                        printf("close %d, ", return_events[index].data.fd);
                        int tmp_ret = epoll_ctl(efd, EPOLL_CTL_DEL, return_events[index].data.fd, NULL);
                        if(tmp_ret == -1)            
                             
                            printf("efd del %d has a error.\n", client_sockfd);        
                             
                        printf("remove descriptor %d successfully.\n", return_events[index].data.fd);
                    
                
             // end of while(1)

            printf("read data finish------\n");
        
    

    free(return_events);
    // close server_sockfd
    shutdown(server_sockfd, 2);

    return 0;

epoll_client.cpp(只发送数据。)

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <resolv.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>

#define BUFSIZE 1024

int main(int argc, char* argv[])

    int sock_clientfd, ret_recvsize, i;
    struct sockaddr_in dest, mine;
    char send_buffer[BUFSIZE + 1];

    // create socket fd
    if ((sock_clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    
        perror("Socket");
        exit(EXIT_FAILURE);
    

    // init server address that client will connetct to.
    bzero(&dest, sizeof(dest));
    dest.sin_family = AF_INET;
    dest.sin_port = htons(9567);
    if(argc != 2)
    
        printf("Usage: %s <dest ip>\n", argv[0]);
        printf("Usage: %s 127.0.0.1\n", argv[0]);
        return -1;
    

    printf("-----\n");

    if (inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0)
    
        perror(argv[1]);
        exit(1);
    

    // connect to server
    printf("will connect!\n");
    if (connect(sock_clientfd, (struct sockaddr *) &dest, sizeof(dest)) != 0)
    
        perror("Connect ");
        exit(EXIT_FAILURE);
    

    while(1)
    
        bzero(send_buffer, BUFSIZE + 1);
        printf("input message:");
        fgets(send_buffer, BUFSIZE, stdin);
        send_buffer[strlen(send_buffer) - 1] = '\0';
        printf("%d\n", strlen(send_buffer));
        int send_retsize = send(sock_clientfd, send_buffer, strlen(send_buffer), 0);
        if(send_retsize == -1)
                                        
            perror("send data to client error happen!");
            exit(EXIT_FAILURE);          
        
        printf("send succ data:%s\n", send_buffer);
        if((send_buffer[0] == 'b')
         && (send_buffer[1] == 'y')
         && (send_buffer[2] == 'e') ) 
        
            printf("client active close connect.\n");
            break;
        
    

    // close sock_clientfd
    close(sock_clientfd);

    return 0;

以下图片是一些运行信息: epoll_server.png

epoll_client.png

服务端读取的数据只有8个字节,难道内核设计的epoll是这样的? 我猜原因如下:

【问题讨论】:

你会发现这个功能很有用:***.com/questions/27527395/… @GiorgiMoniava,我明白了。 您的意见无关紧要。事实上,TCP 没有提供这样的保证。 OT:recv()send() 返回 ssize_t 而不是 int。也无需在 C 中强制转换 void-pointers。 如果recv() 返回0 对方关闭连接。不要再期待任何数据了。离开recv-loop。 【参考方案1】:

您没有在一次读取中收到所有可用内容的原因是因为您一次只能读取 8 个字节。

char* recv_buffer = (char*)malloc(1024+1);
int ret_read = recv(return_events[index].data.fd, recv_buffer, sizeof(recv_buffer), 0);
            // part_3-3-1:read return error

recv_bufferchar* 不是数组,因此 sizeof recv_buffer 等于指针的大小,在您的情况下为 8

请注意,您永远不应依赖包中的数据。如果您的消息协议规定您应该获得 10 个字节,则永远不要期望所有 10 个字节一次都可用。您应该始终以可以处理被拆分为多个读取的数据的方式进行编码。

如果线程处理单个套接字,那么一个简单的do read... while (total_bytes_received &lt; expected_bytes); 就足够了。

如果线程处理多个连接,那么您需要保存已读取的字节,然后继续管理其他准备好的套接字,然后返回到将使用select/epoll 等待更多数据的处理循环。

【讨论】:

@Klas Lindbäck,哦,我明白了。我还有一个问题。除了我的错误,在什么情况下会出现我说的问题描述的?“粘包”还是“不完整阅读”? @study_20160808 我已经添加了有关如何从套接字读取消息以处理消息被拆分为多个 ip 数据包的情况的信息。 @ Klas Lindbäck,我查看了您更新的答案。但我不明白以下情况:我的数据内容:N 大小。最终的 Ip 包大小为 N + M 且 N+M 大小小于 1500 字节。对于这种情况,我的问题会发生吗? @study_20160808 想一想如果服务器很忙并且在服务器开始读取时从客户端收到了几条消息会发生什么。或者如果客户端发送了太多以至于发送缓冲区已满。读取和写入调用都需要假设并非所有数据都可以在一次读取/写入中读取/发送。当您阅读时,可能会获得多于(或少于)一条完整的信息。 @Klas Lindbäck,是的,我明白了,谢谢!

以上是关于在 TCP 套接字程序中,客户端发送一些数据,但服务器需要多次读取。为啥?的主要内容,如果未能解决你的问题,请参考以下文章

TCP套接字服务器如何判断从客户端接收到的数据何时完成?

提升异步 tcp 客户端

TCP Socket在C#中接收数据错误

C TCP套接字,带有文件发送的回显服务器,发送文件后挂断

如何确保 TCP 套接字在创建时连接

《asyncio 系列》8. 在 asyncio 中通过流(StreamReaderStreamWriter)来实现 TCP 请求的发送与接收