【网络编程】基于TCP/IP协议的C/S模型

4

相关视频——C3程序猿-windows网络编程:第一部分tcp/ip

我的小站——半生瓜のblog


@TOC

基于TCP/IP协议的C/S模型

TCP/IP协议

全称——Transmission Control Protocol / Internet Protocol

重要性——TCP/IP协议是今天互联网的基石,没有这个就上不了网

概念——TCP/IP协议族(簇,组,体系),并不是TCP协议和IP协议的总称,指的是整个网络传输体系。而TCP协议和IP协议就是单单的两个协议。

特点——面向可连接的,可靠的,基于数据报的传输协议层


UDP/IP协议——面向非连接的,不可靠的,基于数据报的传输层协议。


Client/Server客户端/服务器模型

C/S模型其实是概念层面的,实现层面可以是基于任何的网络协议。

常见的还有B/S模型——浏览器/服务器模型,基于http/https协议的

套接字编程与socket编程

socket中文——套接字

统称网络编程

使用

  • 局域网
  • 广域网——内网穿透,内网转发

服务端

网络头文件&网络库

是最底层的网络函数,QT、MFC、WPF等封装好的网络库都是对这些最本质的网络函数的二次封装。

不区分大小写(windows)

#include<WinSock2.h>
//第二版的网络库,是一版的升级优化版本
#pragma comment(lib,"ws2_32.lib")
//.lib静态库后缀,是库文件,将.cpp文件编译为二进制文件
//好处:使用时无需编译,直接使用,解决时间
//32位编译环境和64位编译环境都用这个,没有ws2_64

打开网络库

功能

打开网络库/启动网络库,启动了这个库,库里的函数才能使用,功能才能实现。

int WSAStarp(
WORD wVersionRequired,
LPWSADATA lpWSAData
);  

参数1

参数1-使用哪个版本的网络库-WORD-无符号short
    WORD wdVersion = MAKEWORD(2, 1);
//主版本号2存在低数据位,副版本号1存在高数据位

参数前面有lp传地址


参数2

参数2-创建一个结构体,传递给系统,系统将信息放到结构体中,函数调用之后在外面通过结构体查看系统传递给我们的信息。
********************************************************************************
    WSADATA wdSockMsg;
********************************************************************************
其中包括
    struct WSAData {
        WORD                    wVersion;//我们要使用的版本
        WORD                    wHighVersion;//系统能提供给我们的最高的版本
        unsigned short          iMaxSockets;//返回可用的socket数量,2版本之后就没用了
        unsigned short          iMaxUdpDg;//UDP数据报信息的大小,2版本之后就没用了
        char FAR *              lpVendorInfo;//供应上特定的信息,2版本呢之后就没用 了

        char                    szDescription[WSADESCRIPTION_LEN+1];//当前库的描述信息,2.0是第二版的意思
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
    }
********************************************************************************

********************************************************************************
    WSAStartup(wdVersion,&wdSockMsg);
********************************************************************************
当输入的版本不存在
    例如:
    1.3 2.3——有主版本,没有副版本 得到主版本的的最大副版本 1.1 2.2并使用
    3.1 3.3——超过最大版 本号,使用系统能提供的最大版本2.2
    0.0 0.1 0.3——主版本是0,不支持请求的套接字版本

返回值

每一种错误有它唯一的对应码

if (nRes != 0)
    {
        printf("网络库打开失败");
        return 0;
    }

返回值-成功返回0
     -失败返回对应错误的宏
    WSASYSNOTREADY   10091 
        底层网络子系统尚未准备好进行网络通信。                              
        系统配置问题,重启下电脑,检查ws2_32库是否存在,或者是否在环境配置目录下
    WSAVERNOTSUPPORTED 10092 
        此特定Windows套接字实现不提供所请求的Windows套接字支持版本。      
        要使用的版本不支持
    WSAEPROCLIM     10067  
        已达到对Windows套接字实现支持的任务数量的限制。
        Windows Sockets实现可能限制同时使用它的应用程序的数量
    WSAEINPROGRESS  10036          
        正在阻止Windows Sockets 1.1操作。                                                 
        当前函数运行期间,由于某些原因造成阻塞,会返回在这个错误码,其他操作均禁止
    WSAEFAULT       10014          
        lpWSAData参数不是有效指针。                                                     
        参数写错了  

校验版本

HIBYTE(wdSockMsg.wVersion) != 2 || LOBYTE(wdSockMsg.wVersion) != 2
    HIBYTE是高位-副版本
    LOBYTE是低位-主版本
   例如:只要有一个不是2,说明系统不支持我们要的2.2版本

       前面为主版本,后面为副版本
       要打开2.1
HIBYTE(wdSockMsg.wVersion) != 1 && LOBYTE(wdSockMsg.wVersion) != 2

       如果版本不对
       WSACleanup();//清理网络库
       return 0;

创建socket

SOCKET socket(
    int af,
    int type,
    int protocol
);

例如

SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

什么是socket

将底层复杂的协议体系,执行流程,进行封装,封装完的结果,就是一个socket了。

也就是说,socket是我们调用协议进行通信的操作接口。

意义

将复杂的协议过程与编程人员分开,我们只需要操作一个简单那的SOCKET就行了,对于底层的协议过程细节,我们完全不用知道,这就大大的方便了我们。

网络编程难在协议本身的复杂性,简单在我们编程层面完全不用考虑哪些。

本质

就是一种数据类型。就是一个整数。

在这里插入图片描述

socket的值是唯一的,通过这个值找到对应的协议。

应用

网络通信的函数,全都要使用SOCKET,每个客户端有一个SOCKET,服务器有一个SOCKET,通信的时候,就需要这个SOCKET做参数,跟谁通信,就要传递谁的SOCKET。

SOCKET是网络封装的精华,写代码就是不停的使用SOCKET这个变量,所以又叫SOCKET编程。

参数1

地址的类型

加入你要与好友取得联系,可以通过    电话、QQ、微信等方式
AF_INET 2
    ipv4地址
    Internet协议版本地址系列
    例如:192.168.1.103
        0.0.0.0~255.255.255.255
        4字节,32位的地址
        点分十进制表示法
AF_INET6 23
    ipv6地址
    Internet协议版本地址系列
    例如:2001:0:3238:DFE1:63::FEFB
        16字节,128位地址
AF_BTH 32    蓝牙地址    例如:6B:2D:BC:A9:8C:12
AF_IRDA 26    红外数据协会(lrDA)地址

参数2

套接字类型

SOCK_STREAM 1
    提供给带有OOB数据传输机制的顺序,可靠,双向,基于连接的字节流。
    使用TCP作为internet地址系列AF_INET or AF_INET6
SOCK_DGRAM 2
    固定(通常很小)最大长度的无连接,不可靠的缓冲区。
    使用UDP作为internet地址系列AF_INET or AF_INET6
SOCK_RAW 3
    提供允许应用程序操作下一个上层协议头的原始套接字。 
    要操作IPv4标头,必须在套接字上设置IP_HDRINCL套接字选项。
    要操作IPv6标头,必须在套接字上设置IPV6_HDRINCL套接字选项。
SOCK_RDM 4    提供可靠的消息数据报。 
这种类型的一个示例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目。
仅在安装了可靠多播协议时才支持此类型值。
SOCK_SEQPACKET 5    提供基于数据报的伪流数据包。

参数3

协议类型

这个位置写0是什么意思?   
 即系统给我们自动选择合适的协议。但不明确。
IPPROTO_TCP
    传输控制协议(TCP)。 当af参数为AF_INET或AF_INET6且类型参数为SOCK_STREAM时,这是一个可能的值。
    可能的值是什么意思?
    如果有个协议TOP前两个参数也传这样的参数,此时(socket)第三个参数即写成IPPROTO_TOP
IPPROTO_UDP     
用户数据报协议(UDP)。 当af参数为AF_INET或AF_INET6且类型参数为SOCK_DGRAM时,这是一个可能的值。
IPPROTO_ICMP   
 Internet控制消息协议(ICMP)。 
 当af参数为AF_UNSPEC,AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这是一个可能的值。
IPPROTO_IGMP    
Internet组管理协议(IGMP)。 
当af参数为AF_UNSPEC,AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这是一个可能的值。
IPPROTO_RM    
用于可靠多播的PGM协议。 
当af参数为AF_INET且类型参数为SOCK_RDM时,这是一个可能的值。 
在针对Windows Vista及更高版本发布的Windows SDK上,此协议也称为IPPROTO_PGM。
仅在安装了可靠多播协议时才支持此协议值。

返回值

成功-返回可用的socket
失败-不用了一定要释放掉——closesocket(xxx);
    然后再WSACleanup();清理网络库
    注意二者的先后顺序,一定要先释放,然后再清理网路库,
    因为closesocket()是网络库中的函数。
********************************************************************************
失败——返回INVALID_SOCKET
   if (INVALID_SOCKET == socketServer)
    {
        int a = WSAGetLastError();//获取错误码
        WSACleanup();
        return 0;
    }
   //获取错误码——int a = WSAGetLastError();
   //检测在它上面离它最近的错误码    

绑定地址与端口

int bind(   SOCKET s,   const sockaddr* addr,   int namelen);

作用

给我们的socket绑定端口号和具体地址

地址:找到电脑,理论上只有一个。

端口号:找到电脑上对应软件的具体功能,每个通信的端口号是唯一的,同一个软件可能占用多个端口号。

参数1

传递上面创建好的socket

(scoket绑定好地址类型、socket类型,协议类型)

(bind绑定实质的地址、端口号)

参数2

在这里插入图片描述

struct sockaddr {
        ushort  sa_family;//地址类型
        char    sa_data[14];//端口号 ip地址
    //往一个字符串中赋值端口号和ip地址不好赋所以给出sockaddr_in,与之对应
};//16个字节

struct sockaddr_in {
        short   sin_family;//地址类型
        u_short sin_port;//端口号
        struct  in_addr sin_addr;//ip地址,4字节
        char    sin_zero[8];
};//16字节
//两个结构体大小和内存排布一样
结构体
    -地址类型
     -ip地址  127.0.0.1-回送地址 本地回环地址 本地网络测试
              192.168.xxx.xxx- 用户ip地址
     -端口号  就是一个整数,0~65535。unsigned short
        理论上0~65535都可以,但是0~1023为系统保留占用端口号
            21端口分配给FTP(文件传输协议)服务
            25端口分配给SMTP(简单邮件传输协议)服务
            80端口分配给HTTP服务
        所以真正的范围是1024~65535
        端口是唯一的。
    打开cmd,输入netstat -ano 查看被使用的所有端口
                netstat -aon|findstr "12345"检查我们要使用的端口是否被占用

SOCKETADDR_IN为sockaddr提供方便
创建一个结构体SOCKETADDR_IN
为其中的结构体成员赋值
然后将它强转成sockaddr添加成功

    SOCKADDR_IN si;
    si.sin_family = AF_INET;//地址类型
    si.sin_port = htons(12345);//端口-将输入的unsigned short转换
    si.sin_addr.S_un.S_addr = inet_addr("127,0,0,1");//ip地址

    bind(socketServer, (const struct sockaddr*)&si, sizeof(si));

返回值

成功-返回0
失败-失败 返回宏SOCKET_ERROR
            具体错误码通过WSAGetLastError()获得

if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si)))
{
    int a = WSAGetLastError();  
    //释放
    closesocket(socketServer);
    //关闭网络库
    WSACleanup();
    return 0;
}

开始监听

int WSAAPI listen(  SOCKET s,   int backing);
listen(socketServer, SOMAXCONN);

作用

将套接字置于正在侦听传入连接的状态。

参数1

服务器端的socket,也就是socket函数创建的。

参数2

挂起连接的最大长度。(排队等待区)休息区的长度。

可以手动设置,可能是2~10,一般是SOMAXCONN让系统自己选择最合适的个数。不同系统的环境不一样,所以这个合适的数也都不一样。

WSAAPI

调用约定,是给操作系统看的,我们可以忽略它。

决定-函数名字的编译方式-参数的入栈顺序-函数的调用时间。

返回值

成功-返回0
失败-返回宏SOCKET_ERROR
     if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
    {
        //出错了
        int a = WSAGetLastError();
        //释放
        closesocket(socketServer);
        //关闭网络库
        WSACleanup();
        return 0;
    }

创建客户端socket/接收连接

将每个客户端的信息都创建成一个socket。

SOCKET WSAAPI accept(
    SOCKET s,
    sockaddr * addr,
    int *addrlen
);

作用

accept函数允许在套接字上进行传入连接尝试。

listen监听客户端来的链接,accept将客户端的信息绑定到一个socket上,也就是给客户端创建一个socket,通过返回值得到客户端socket。

一次只能创建一个(返回值1个),有几个客户端链接,就要调用几次。

参数1

(服务器socket)

参数2

客户端的地址端口信息结构体,同bind的第二个参数

意义:系统帮我们监视着客户端的动态,肯定会记录客户端的信息,也就是IP地址,和端口号,并通过这个结构体记录。

只是这个我们不用自己填写结构体中的内容,系统帮我们填写

    //创建客户端
    struct sockaddr_in clientMsg;//客户端信息
    int len = 0;
    SOCKET socketClient = accept(socketServer, (struct sockaddr*)&clientMsg, &len);

参数3

参数2的大小


参数2、3也可以都写成NULL,那就是不直接得到客户端的地址、端口号。

可以通过
    getpeername(newSocket, (struct sockaddr*)&sockClient, &nLen);
得到客户端信息
    通过
    getsockname(sSocket, (sockaddr*)&addr, &nLen);
得到本地服务器信息

返回值

成功-返回客户端socket
失败-INVALID_SOCKET
    if (socketClient == INVALID_SOCKET)
    {
        closesocket(socketServer);
        closesocket(socketClient);
        WSACleanup();//清理网络库
        return 0;
    }

accept调试

1.阻塞,同步,当服务器没有客户端链接时,它会一直等待。

2.多个链接,一次只能连接一个,5个就要循环5次。同时客户端socket也要创建成数组,否则上一个的socket就丢了。

与客户端收发消息

消息从谁那来,要发送给谁,就写谁的socket

得到指定客户端(参数1)发来的消息

int recv(
    SOCKET s,
    char *buf,//消息,按字节
    int len,//长度
    int flags
);
例:
    char buf[1500] = { 0 };
    int res = recv(socketClient, buf, 1499, 0);
原理

本质就是复制。

数据的接收都是由协议本身做的,也就是socket的底层做的,系统会有一段缓冲区,存储着收到的数据。

recv的作用就是通过socket找到这个缓冲区,并把数据赋值进参数2。

参数1

客户端的socket,每个客户端对应唯一的socket

参数2

客户端消息的存储空间,是个字符数组,一般是1500字节。

网络传输的最大单元是1500字节,也就是客户端发过来的数据,一次最大就是1500字节,这是协议规定,很多情况总结出来的最优值。

参数3

想要读取的字节个数。

一般是参数2的字节数-1,把/0字符串结尾留出来。

参数4

数据的读取方式

一般就写个0。

0
正常逻辑(自然性质)
        从系统缓冲区里读,读走几个删几个,要不每次都从头开始读。
MSG_PEEK
        读完不删
        不建议使用
MSG_OOB
        带外数据
        就是传输一段数据,在外带一个额外的特殊数据。(小声bb)
        不建议使用,读数据不行,无法计数。
NSG_WAITALL     直到系统缓冲区字节数满足参数3所请求的字节数,才开始读取。
返回值
返回读出来字节大小,读没了就在recv函数卡着,等着客户端发来数据,即阻塞,同步。死等
客户端下线,返回0。
执行失败,返回SOCKET_ERROR    

    if (res == 0)//客户端下线,链接中断
    {
        printf("链接中断、客户端下线\n");
    }
    else if (res == SOCKET_ERROR)
    {
        printf("出错了\n");
        int a = WSAGetLastError();
        //根据实际情况处理
    }
    else
    {
        printf("%d  %s\n", res, buf);
    } 

int WSAAPI send(    SOCKET s,   const char* buf,    int len,    int flags);
作用

向目标发送数据

本质:send函数将我们的数据复制粘贴进系统的协议发送缓冲区,计算机伺机发出去。

传输单元是1500字节。

参数1

目标的socket,每个客户端对应唯一的socket

参数2

给对方发送的字节串。1500

这个大小不同的协议不一样,
链路层14字节,ip头20字节,tcp头20字节,数据结尾还要有状态确认,加起来也几十个字节,
数据结尾还要要状态确认,加起来也几十个字节,所以实际的数据位,不能写1500个,要留出来,
例如1024,或者最多写1400,别多于1400是最好的。
超过1500
    系统会分片处理,比如2000个字节
    系统分成两个包,1400+包头 == 1500 假设包头100字节
                    600+包头 == 700
                    分两次发送出去
    结果
        系统要分包再打包,再发送,客户端收到之后还得拆包,组合数据,
        从而增加了系统的工作,降低了效率。
        有的协议,就把分片后的二包直接丢了。
参数3

字节个数。1400

参数4
0
MSG_OOB 同recv
MSG_DIBTROUTE指定数据不应受路由限制,Windows套接字服务提供程序可以选择忽略此标志。
返回值
成功-返回写入的字节数
失败-返回SOCKET_ERROR
        WSAGetLastError()得到错误码
        根据错误码信息做相应处理
        -重启
        -等待
        -不用理会

完整代码

#define _WINSOCK_DEPRECATED_NO_WARNINGS 
#define _CRT_SECURE_NO_WARNINGS
#include<WinSock2.h>
#include<stdio.h>
#include<string.h>
#pragma comment(lib,"ws2_32.lib")

int main(void)
{
    //打开网络库 
    WORD wdVersion = MAKEWORD(2, 2);
    WSADATA wdSockMsg;
    int nRes = WSAStartup(wdVersion,&wdSockMsg);

    if (nRes != 0)
    {
        printf("网络库打开失败");
        return 0;
    }
    //校验版本
    if (HIBYTE(wdSockMsg.wVersion) != 2 || LOBYTE(wdSockMsg.wVersion) != 2)
    {
        //说明版本不对
        printf("版本不对");
        WSACleanup();//清理网络库
        return 0;
    }
    //创建socket
    SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

    if (INVALID_SOCKET == socketServer)
    {
        printf("创建服务器socket失败");
        int a = WSAGetLastError();//获取错误码
        WSACleanup();
        return 0;
    }

    SOCKADDR_IN si;
    si.sin_family = AF_INET;//地址类型
    si.sin_port = htons(12345);//端口-将输入的unsigned short转换
    si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//ip地址

    if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr *)&si, sizeof(si)))
    {
        printf("绑定错误");
        int a = WSAGetLastError();

        //释放
        closesocket(socketServer);
        //关闭网络库
        WSACleanup();
        return 0;
    }

    //开始监听
    if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
    {
        //出错了
        int a = WSAGetLastError();
        //释放
        closesocket(socketServer);
        //关闭网络库
        WSACleanup();
        return 0;
    }

    //创建客户端
    struct sockaddr_in clientMsg;//客户端信息
    int len = sizeof(clientMsg);
    SOCKET socketClient = accept(socketServer, (struct sockaddr*)&clientMsg, &len);
    if (socketClient == INVALID_SOCKET)
    {
        printf("客户端链接失败");
        closesocket(socketServer);
        closesocket(socketClient);
        WSACleanup();//清理网络库
        return 0;
    }
    printf("客户端链接成功\n");

    //收发消息

    int sValue = send(socketClient, "服务器链接成功", sizeof("服务器链接成功"), 0);
    if (sValue == SOCKET_ERROR)
    {
        int a = WSAGetLastError();
        closesocket(socketClient);
        closesocket(socketServer);
        WSACleanup();
        return 0;
     }

    while (1)
    {
        char buf[1500] = { 0 };
        int res = recv(socketClient, buf, 50, 0);
        if (res == 0)//客户端下线,链接中断
        {
            printf("链接中断、客户端下线\n");
        }
        else if (res == SOCKET_ERROR)
        {
            printf("出错了\n");
            int a = WSAGetLastError();
            //根据实际情况处理
        }
        else
        {
            printf("客户端说:  %d  %s\n", res, buf);
        }

        scanf("%s", buf);
        send(socketClient, buf,strlen(buf), 0);
    }

    closesocket(socketServer);
    closesocket(socketClient);
    WSACleanup();//清理网络库

    system("pause");
    return 0;
}

客户端

网络库头文件 网络库
    打开网络库
    校验版本
    创建SOCKET

链接到服务器

int WSAAPI connect(
    SOCKET s,
    const sockaddr* name,
    int namelen
);
struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;
serverMsg.sin_port = htons(12345);//转换成网络字节序
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//服务器ip地址

connect(socketServer, (struct sockaddr*)&serverMsg, sizeof(serverMsg));

作用

链接服务器并把服务器socket绑定到一起。

参数1

服务器socket

参数2

服务器ip地址端口号结构体

参数3

参数2结构体大小

返回值

成功-返回0
失败-返回SOCKET_ERROR

客户端与服务器收发消息,一发一接,一发一接,对应。


完整代码

#define _WINSOCK_DEPRECATED_NO_WARNINGS 
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<WinSock2.h>
#pragma  comment(lib,"Ws2_32.lib")
int main(void)
{
    WORD wdVersion = MAKEWORD(2, 2);
    WSADATA wdSockMsg;
    int nRes = WSAStartup(wdVersion, &wdSockMsg);
    if (nRes != 0)
    {
        printf("网络库打开失败");
        return 0;
    }
    //校验版本
    if (HIBYTE(wdSockMsg.wVersion) != 2 || LOBYTE(wdSockMsg.wVersion) != 2)
    {
        //说明版本不对
        printf("版本不对");
        WSACleanup();//清理网络库
        return 0;
    }
    //创建的是服务器的,客户端不用创建自己的socket,因为链接服务器,服务器会创建出来客户端的socket
    SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (INVALID_SOCKET == socketServer)
    {
        int a = WSAGetLastError();
        WSACleanup();
        return 0;
    }

    //链接服务器
    struct sockaddr_in serverMsg;
    serverMsg.sin_family = AF_INET;
    serverMsg.sin_port = htons(12345);//转换成网络字节序
    serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//服务器ip地址

    int cValue = connect(socketServer, (struct sockaddr*)&serverMsg, sizeof(serverMsg));
    if (cValue == SOCKET_ERROR)
    {
        int a = WSAGetLastError();
        closesocket(socketServer);
        WSACleanup();
        return 0;
    }

    //收发消息

    while (1)
    {
        char buf[1500] = { 0 };
        int res = recv(socketServer, buf, 50, 0);

        if (res == 0)//客户端下线,链接中断
        {
            printf("链接中断、客户端下线\n");
        }
        else if (res == SOCKET_ERROR)
        {
            printf("出错了\n");
            int a = GetLastError();
            //根据实际情况处理
        }
        else
        {
            printf("服务器说:  %d  %s\n", res, buf);
        }

        scanf("%s", buf);
        int sValue = send(socketServer, buf, strlen(buf), 0);
        if (sValue == SOCKET_ERROR)
        {
            int a = WSAGetLastError();
            closesocket(socketServer);
            WSACleanup();
            return 0;
        }
    }

    //清理网络库
    closesocket(socketServer);
    WSACleanup();
    system("pause");
    return 0;
}

延伸

由于accept,recv是阻塞的,做其中一件事,另一件事就做不了,等着接收客户端的消息-recv,这时来了个链接请求-accept无法处理。(没有目标的等待)

正确的处理方式是——哪个socket有请求就处理谁,得到连接请求,我们就直接accept,得到发来的消息,就recv。(有目的的等待,处理有请求的)

引出select模型