Go最新socket 的阻塞模式和非阻塞模式_socket阻塞和非阻塞(3),Golang源码的Binder权限是如何控制

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

}

//3. 不断向服务器发送数据,或者出错退出
int count = 0;
while (true)
{
    int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
    if (ret != strlen(SEND_DATA))
    {
        std::cout << "send data error." << std::endl;
        break;
    } 
    else
    {
        count ++;
        std::cout << "send data successfully, count = " << count << std::endl;
    }
}

//5. 关闭socket
close(clientfd);

return 0;

}


在 shell 中分别编译这两个 cpp 文件得到两个可执行程序 **blocking\_server** 和 **blocking\_client**:



g++ -g -o blocking_server blocking_server.cpp
g++ -g -o blocking_client blocking_client.cpp


我们先启动 **blocking\_server**,然后用 gdb 启动 **blocking\_client**,输入 **run** 命令让 **blocking\_client**跑起来,**blocking\_client** 会不断地向 **blocking\_server** 发送"**helloworld**"字符串,每次 send 成功后,会将计数器 **count** 的值打印出来,计数器会不断增加,程序运行一段时间后,计数器 **count** 值不再增加且程序不再有输出。操作过程及输出结果如下:


**blocking\_server** 端:



[root@localhost testsocket]# ./blocking_server
accept a client connection.
[root@localhost testsocket]# gdb blocking_client
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-100.el7_4.1
Copyright © 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type “show copying”
and “show warranty” for details.
This GDB was configured as “x86_64-redhat-linux-gnu”.
For bug reporting instructions, please see:
http://www.gnu.org/software/gdb/bugs/
Reading symbols from /root/testsocket/blocking_client…done.
(gdb) run
//输出结果太多,省略部分…
send data successfully, count = 355384
send data successfully, count = 355385
send data successfully, count = 355386
send data successfully, count = 355387
send data successfully, count = 355388
send data successfully, count = 355389
send data successfully, count = 355390


此时程序不再有输出,说明我们的程序应该“卡在”某个地方,继续按 **Ctrl + C** 让 gdb 中断下来,输入 **bt** 命令查看此时的调用堆栈,我们发现我们的程序确实阻塞在 **send** 函数调用处:



^C
Program received signal SIGINT, Interrupt.
0x00007ffff72f130d in send () from /lib64/libc.so.6
(gdb) bt
#0 0x00007ffff72f130d in send () from /lib64/libc.so.6
#1 0x0000000000400b46 in main (argc=1, argv=0x7fffffffe598) at blocking_client.cpp:41
(gdb)


上面的示例验证了如果一端一直发数据,而对端应用层一直不取数据(或收取数据的速度慢于发送速度),则很快两端的内核缓冲区很快就会被填满,导致发送端调用 send 函数被阻塞。这里说的“**内核缓冲区**” 其实有个专门的名字,即 TCP 窗口。也就是说 socket 阻塞模式下, send 函数在 TCP 窗口太小时的行为是阻塞当前程序执行流(即阻塞 send 函数所在的线程的执行)。


说点题外话,上面的例子,我们每次发送一个“**helloworld**”(10个字节),一共发了 355390 次(每次测试的结果略有不同),我们可以粗略地算出 TCP 窗口的大小大约等于 1.7 M左右 (10 \* 355390 / 2)。


让我们再深入一点,我们利用 Linux tcpdump 工具来动态看一下这种情形下 TCP 窗口大小的动态变化。需要注意的是,Linux 下使用 tcpdump 这个命令需要有 root 权限。


我们开启三个 shell 窗口,在第一个窗口先启动 **blocking\_server** 进程,在第二个窗口用 tcpdump 抓经过 TCP 端口 3000 上的数据包:



[root@localhost testsocket]# tcpdump -i any -nn -S ‘tcp port 3000’
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes


接着在第三个 shell 窗口,启动 **blocking\_client**。当 **blocking\_client** 进程不再输出时,我们抓包的结果如下:



[root@localhost testsocket]# tcpdump -i any -nn -S ‘tcp port 3000’
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
11:52:35.907381 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [S], seq 1394135076, win 43690, options [mss 65495,sackOK,TS val 78907688 ecr 0,nop,wscale 7], length 0
20:32:21.261484 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [S.], seq 1233000591, ack 1394135077, win 43690, options [mss 65495,sackOK,TS val 78907688 ecr 78907688,nop,wscale 7], length 0
11:52:35.907441 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 0
11:52:35.907615 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [P.], seq 1394135077:1394135087, ack 1233000592, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 10
11:52:35.907626 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1394135087, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 0
11:52:35.907785 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [P.], seq 1394135087:1394135097, ack 1233000592, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 10
11:52:35.907793 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1394135097, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 0
11:52:35.907809 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [P.], seq 1394135097:1394135107, ack 1233000592, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 10
11:52:35.907814 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1394135107, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 0
…内容太长, 部分省略…
11:52:40.075794 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395013717, win 374, options [nop,nop,TS val 78911856 ecr 78911816], length 0
11:52:40.075829 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [P.], seq 1395013717:1395030517, ack 1233000592, win 342, options [nop,nop,TS val 78911856 ecr 78911856], length 16800
11:52:40.115847 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395030517, win 305, options [nop,nop,TS val 78911896 ecr 78911856], length 0
11:52:40.115866 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [P.], seq 1395030517:1395047317, ack 1233000592, win 342, options [nop,nop,TS val 78911896 ecr 78911896], length 16800
11:52:40.155703 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395047317, win 174, options [nop,nop,TS val 78911936 ecr 78911896], length 0
11:52:40.155752 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [P.], seq 1395047317:1395064117, ack 1233000592, win 342, options [nop,nop,TS val 78911936 ecr 78911936], length 16800
11:52:40.195132 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395064117, win 43, options [nop,nop,TS val 78911976 ecr 78911936], length 0
11:52:40.435748 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [P.], seq 1395064117:1395069621, ack 1233000592, win 342, options [nop,nop,TS val 78912216 ecr 78911976], length 5504
11:52:40.435782 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395069621, win 0, options [nop,nop,TS val 78912216 ecr 78912216], length 0
11:52:40.670661 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78912451 ecr 78912216], length 0
11:52:40.670674 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395069621, win 0, options [nop,nop,TS val 78912451 ecr 78912216], length 0
11:52:41.141703 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78912922 ecr 78912451], length 0
11:52:42.083643 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78913864 ecr 78912451], length 0
11:52:42.083655 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395069621, win 0, options [nop,nop,TS val 78913864 ecr 78912216], length 0
11:52:43.967506 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78915748 ecr 78913864], length 0
11:52:43.967532 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395069621, win 0, options [nop,nop,TS val 78915748 ecr 78912216], length 0
11:52:47.739259 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78919520 ecr 78915748], length 0
11:52:47.739274 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395069621, win 0, options [nop,nop,TS val 78919520 ecr 78912216], length 0
11:52:55.275863 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78927056 ecr 78919520], length 0
11:52:55.275931 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [.], ack 1395069621, win 0, options [nop,nop,TS val 78927056 ecr 78912216], length 0


抓取到的前三个数据包是 **blocking\_client** 与 **blocking\_server** 建立三次握手的过程。



11:52:35.907381 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [S], seq 1394135076, win 43690, options [mss 65495,sackOK,TS val 78907688 ecr 0,nop,wscale 7], length 0
20:32:21.261484 IP 127.0.0.1.3000 > 127.0.0.1.40846: Flags [S.], seq 1233000591, ack 1394135077, win 43690, options [mss 65495,sackOK,TS val 78907688 ecr 78907688,nop,wscale 7], length 0
11:52:35.907441 IP 127.0.0.1.40846 > 127.0.0.1.3000: Flags [.], ack 1233000592, win 342, options [nop,nop,TS val 78907688 ecr 78907688], length 0


示意图如下:


[![img](https://img-blog.csdnimg.cn/img_convert/d65c2df82a724c15bbbd685c3495ea4a.webp?x-oss-process=image/format,png)](https://github.com/balloonwj/CppGuide/blob/master/articles/imgs/socketmode2.webp)


当每次 **blocking\_client** 给 **blocking\_server** 发数据以后,**blocking\_server** 会应答 **blocking\_server**,在每次应答的数据包中会带上自己的当前可用 TCP 窗口大小(看上文中结果从 **127.0.0.1.3000 > 127.0.0.1.40846**方向的数据包的 **win** 字段大小变化),由于 TCP 流量控制和拥赛控制机制的存在,**blocking\_server** 端的 TCP 窗口大小短期内会慢慢增加,后面随着接收缓冲区中数据积压越来越多, TCP 窗口会慢慢变小,最终变为 0。


另外,细心的读者如果实际去做一下这个实验会发现一个现象,即当 tcpdump 已经显示对端的 TCP 窗口是 0 时, **blocking\_client** 仍然可以继续发送一段时间的数据,此时的数据已经不是在发往对端,而是逐渐填满到本端的内核发送缓冲区中去了,这也验证了 send 函数实际上是往内核缓冲区中拷贝数据这一行为。


我们再来验证一下非阻塞 socket 的 send 行为,**server** 端的代码不变,我们将 **blocking\_client.cpp** 中 socket 设置成非阻塞的,修改后的代码如下:



/**
* 验证非阻塞模式下send函数的行为,client端,nonblocking_client.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include
#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;
}

//3. 不断向服务器发送数据,或者出错退出
int count = 0;
while (true)
{
    int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
    if (ret == -1) 
    {
        //非阻塞模式下send函数由于TCP窗口太小发不出去数据,错误码是EWOULDBLOCK
        if (errno == EWOULDBLOCK)
        {
            std::cout << "send data error as TCP Window size is too small." << std::endl;
            continue;
        } 
        else if (errno == EINTR)
        {
            //如果被信号中断,我们继续重试
            std::cout << "sending data interrupted by signal." << std::endl;
            continue;
        } 
        else 
        {
            std::cout << "send data error." << std::endl;
            break;
        }
    }
    if (ret == 0)
    {
        //对端关闭了连接,我们也关闭
        std::cout << "send data error." << std::endl;
        close(clientfd);
        break;
    } 
    else
    {
        count ++;
        std::cout << "send data successfully, count = " << count << std::endl;
    }
}

//5. 关闭socket
close(clientfd);

return 0;

}


编译 **nonblocking\_client.cpp** 得到可执行程序 **nonblocking\_client**:



g++ -g -o nonblocking_client nonblocking_client.cpp


运行 **nonblocking\_client**,运行一段时间后,由于对端和本端的 TCP 窗口已满,数据发不出去了,但是 send 函数不会阻塞,而是立即返回,返回值是 **-1**(Windows 系统上 返回 SOCKET\_ERROR,这个宏的值也是 **-1**),此时得到错误码是 **EWOULDBLOCK**。执行结果如下:


[[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0KzNqgUX-1680355658265)(null)]]( )


在了解了 send 函数的行为,我们再来看一下阻塞模式下的 recv 函数行为。服务器端代码不需要修改,我们修改一下客户端代码,如果服务器端不给客户端发数据,此时客户端调用 recv 函数执行流会阻塞在 recv 函数调用处。继续修改一下客户端代码:



/**
* 验证阻塞模式下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
#include <string.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;
}

//直接调用recv函数,程序会阻塞在recv函数调用处
char recvbuf[32] = {0};
int ret = recv(clientfd, recvbuf, 32, 0);
if (ret > 0) 
{
    std::cout << "recv successfully." << std::endl;
} 
else 
{
    std::cout << "recv data error." << std::endl;
}

//5. 关闭socket
close(clientfd);

return 0;

}


编译 **blocking\_client\_recv.cpp** 并使用启动,我们发现程序既没有打印 recv 调用成功的信息也没有调用失败的信息,将程序中断下来,使用 **bt** 命令查看此时的调用堆栈,发现程序确实阻塞在 recv 函数调用处。



[root@localhost testsocket]# g++ -g -o blocking_client_recv blocking_client_recv.cpp
[root@localhost testsocket]# gdb blocking_client_recv
Reading symbols from /root/testsocket/blocking_client_recv…done.
(gdb) r
Starting program: /root/testsocket/blocking_client_recv
^C
Program received signal SIGINT, Interrupt.
0x00007ffff72f119d in recv () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64 libgcc-4.8.5-16.el7_4.2.x86_64 libstdc+±4.8.5-16.el7_4.2.x86_64
(gdb) bt
#0 0x00007ffff72f119d in recv () from /lib64/libc.so.6
#1 0x0000000000400b18 in main (argc=1, argv=0x7fffffffe588) at blocking_client_recv.cpp:40


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



/**
* 验证阻塞模式下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
#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)

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

std::endl;
break;
}
else if (ret == -1)
{
if (errno == EWOULDBLOCK)

[外链图片转存中…(img-0IVvRmJ8-1715887305343)]
[外链图片转存中…(img-evVINxXq-1715887305343)]
[外链图片转存中…(img-JSaMLiRB-1715887305343)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值