socket send和recv正确用法

socket 非阻塞模式下的 recv 行为

非阻塞模式下如果当前无数据可读,recv 函数将立即返回,返回值为 -1,错误码为 EWOULDBLOCK。将客户端代码修成一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
 * 验证阻塞模式下recv函数的行为,client端,blocking_client_recv.cpp
 * zhangyl 2018.12.17
 */
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT     3000
#define SEND_DATA       "helloworld"

int main(int argc, char* argv[])
{
    //1.创建一个socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1)
    {
        std::cout << "create client socket error." << std::endl;
        return -1;
    }

    //2.连接服务器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);
    if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
    {
        std::cout << "connect socket error." << std::endl;
		close(clientfd);
        return -1;
    }
	
	//连接成功以后,我们再将 clientfd 设置成非阻塞模式,
	//不能在创建时就设置,这样会影响到 connect 函数的行为
	int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
	int newSocketFlag = oldSocketFlag | O_NONBLOCK;
	if (fcntl(clientfd, F_SETFL,  newSocketFlag) == -1)
	{
		close(clientfd);
		std::cout << "set socket to nonblock error." << std::endl;
		return -1;
	}
	
	//直接调用recv函数,程序会阻塞在recv函数调用处
	while (true)
	{
		char recvbuf[32] = {0};
		int ret = recv(clientfd, recvbuf, 32, 0);
		if (ret > 0) 
		{
			//收到了数据
			std::cout << "recv successfully." << std::endl;
		} 
		else if (ret == 0)
		{
			//对端关闭了连接
			std::cout << "peer close the socket." << std::endl;	
			break;
		} 
		else if (ret == -1) 
		{
			if (errno == EWOULDBLOCK)
			{
				std::cout << "There is no data available now." << std::endl;
			} 
			else if (errno == EINTR) 
			{
				//如果被信号中断了,则继续重试recv函数
				std::cout << "recv data interrupted by signal." << std::endl;				
			} else
			{
				//真的出错了
				break;
			}
		}
	}
	
	//5. 关闭socket
	close(clientfd);

    return 0;
}

执行结果与我们预期的一模一样, recv 函数在无数据可读的情况下并不会阻塞情绪,所以程序会一直有“There is no data available now.”相关的输出。

 

recv 指定长度


// recieve buffer
int RecvBuffer(int nSockfd, char *pBuffer, size_t nLength, size_t &nReceived)
{
    LOG_ENTER;
    size_t nReceive = 0;
    int nRetry = 5;
    nReceived = 0;
    while (true && nRetry > 0)
    {
        // receive buffer
        nReceive = recv(nSockfd, pBuffer+nReceived, nLength-nReceived, 0);
        if (nReceive == -1)
        {
            LOG_ERROR("recv failed!errno=" << errno << "sockfd=" << nSockfd << ",buffer=" << pBuffer);
            switch (errno)
            {
                case EAGAIN:
                case EINTR:
                {
                    nRetry--;
                    usleep(10);
                    continue;
                }

                case EBADF:
                {
                    return 0;
                }

                default:
                {
                    return -1;
                }
            }
        }
        else if (nReceive == 0)
        {
            LOG_ERROR("recv failed!errno=" << errno << "sockfd=" << nSockfd << ",buffer=" << pBuffer);
            return 0;
        }
        else
        {
            // increase received bytes
            nReceived += nReceive;
            // test if be finished or not
            if (nReceived >= nLength)
            {
                return nReceived;
            }
        }
    }
    return -1;
}
 

非阻塞模式下 SEND 和 RECV 函数的返回值总结

我们来根据前面的讨论来总结一下 send 和 recv 函数的各种返回值意义:

返回值 n返回值含义
大于 0成功发送 n 个字节
0对端关闭连接
小于 0( -1)出错或者被信号中断或者对端 TCP 窗口太小数据发不出去(send)或者当前网卡缓冲区已无数据可收(recv)

我们来逐一介绍下这三种情况:

  • 返回值大于 0

    对于 send 和 recv 函数返回值大于 0,表示发送或接收多少字节,需要注意的是,在这种情形下,我们一定要判断下 send 函数的返回值是不是我们期望发送的缓冲区长度,而不是简单判断其返回值大于 0。举个例子:

    1
    2
    3
    4
    5
    
    int n = send(socket, buf, buf_length, 0);
    if (n > 0)
    {
        printf("send data successfully\n");
    }
    

    很多新手会写出上述代码,虽然返回值 n 大于 0,但是实际情形下,由于对端的 TCP 窗口可能因为缺少一部分字节就满了,所以返回值 n 的值可能在 (0, buf_length] 之间,当 0 < n < buf_length 时,虽然此时 send 函数是调用成功了,但是业务上并不算正确,因为有部分数据并没发出去。你可能在一次测试中测不出 n 不等于 buf_length 的情况,但是不代表实际中不存在。所以,建议要么认为返回值 n 等于 buf_length 才认为正确,要么在一个循环中调用 send 函数,如果数据一次性发不完,记录偏移量,下一次从偏移量处接着发,直到全部发送完为止。

    1
    2
    3
    4
    5
    6
    
    //不推荐的方式一
    int n = send(socket, buf, buf_length, 0);
    if (n == buf_length)
    {
        printf("send data successfully\n");
    }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//推荐的方式二:在一个循环里面根据偏移量发送数据
bool SendData(const char* buf, int buf_length)
{
    //已发送的字节数目
    int sent_bytes = 0;
    int ret = 0;
    while (true)
    {
        ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0);
        if (ret == -1)
        {
            if (errno == EWOULDBLOCK)
            {
                //严谨的做法,这里如果发不出去,应该缓存尚未发出去的数据,后面介绍
                break;
            }
            else if (errno == EINTR)
                continue;
            else
                return false;
        }
        else if (ret == 0)
        {
            return false;
        }

        sent_bytes += ret;
        if (sent_bytes == buf_length)
            break;

        //稍稍降低 CPU 的使用率
        usleep(1);
    }

    return true;
}

  • 返回值等于 0

    通常情况下,如果 send 或者 recv 函数返回 0,我们就认为对端关闭了连接,我们这端也关闭连接即可,这是实际开发时最常见的处理逻辑。

  • 返回值小于 0

    对于 send 或者 recv 函数返回值小于 0 的情况(即返回 -1),根据前文的讨论,此时并不表示 send 或者 recv 函数一定调用出错。这里列一个表格说明:

     返回值和错误码send 函数recv 函数操作系统说明
    1返回 -1,错误码 EWOUDBLOCK 或 EAGAINTCP 窗口太小,数据暂时发不出去当前内核缓冲区中无可读数据Linux
    2返回 -1,错误码 EINTR被信号中断,需要重试被信号中断,需要重试Linux
    3返回 -1,错误码不是上述 1 和 2出错出错Linux
    4返回 -1,错误码 WSAEWOUDBLOCKTCP 窗口太小,数据暂时发不出去当前内核缓冲区中无可读数据Windows
    5返回 -1,错误码不是上述 4出错出错Windows

注意:这里是针对非阻塞模式下 socket 的 send 和 recv 返回值,如果是阻塞模式下 socket,如果返回值是 -1(Windows 上即 SOCKET_ERROR),则一定表示出错。

阻塞与非阻塞的 SOCKET 的各自适用场景

阻塞的 socket 函数在调用 sendrecvconnectaccept 等函数时,如果特定的条件不满足,就会阻塞其调用线程直至超时,非阻塞的 socket 恰恰相反。这并不意味着非阻塞模式的 socket 模式比阻塞模式的 socket 模式好,二者各有优缺点。

非阻塞模式的 socket,一般用于需要支持高并发多 QPS 的场景下(如服务器程序),但是正如前文所述,这种模式让程序执行流和控制逻辑变复杂;相反,阻塞模式逻辑简单,程序结构简单明了,常用于一些特殊的场景。这里举两个应用的场景:

示例一

程序需要临时发送一个文件,文件分段发送,每段对端都会的给与一个应答,程序可以单独开一个任务线程,在这个任务线程函数里面,使用先 send 后 recv 再 send 再 recv 的模式,每次 send 和 recv 都是阻塞式的。

示例二

A 端与 B 端之间只有问答模式,即 A 发送给 B 一个请求,B 必定会应答给 A 一个响应,除此以外,B 不会给 A 推送任何数据,也可以采取阻塞模式,每次 send 完请求后,就可以直接使用阻塞式的 recv 去接受一定要有的应答包。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值