大家好,我是一多,今天是东北的小年(2023/1/14),发一篇随笔证明我还活着吧(好久没更新了)
本文讲的是windows上的套接字编程中的基于select的I/O复用模型的原理,之后对select模型进行了编程及抓包分析。
1 基础知识
1.1 winsock库简介
套接字(socket)最早是unix系列操作系统为网络编程提供的系统调用,而windows移植了unix对套接字的实现,并封装到动态链接库(如ws2_32.dll),并将这些动态链接库作为网络编程的函数调用,而这个windows的套接字简称为winsock。
套接字是内核将一个个的基于TCP/IP协议栈的网络通信用一个个套接字分别管理,并返回给用户程序不同的套接字描述符(fd),同时提供给用户程序一系列系统或函数调用的api。用户程序可以调用这些api,对套接字描述符所对应的网络连接进行网络连接的控制以及网络数据的读写。
这些api,如socket和shutdown、bind、listen、connect和accept等进行网络连接的控制;如send和sendto、recv和recvfrom进行网络数据的读写。
和unix/linux的套接字不同,windows套接字winsock在调用函数调用前,需要先链接动态链接库,这个过程分两步:
第1步:添加编译的链接参数:
-lws2_32 -lmswsock -ladvapi32
第2步:引入头文件:
#include<winsock2.h> #include<ws2tcpip.h>
这样用户程序就可以调用winsock的函数调用了。
1.2 套接字及套接字通信过程
最初没有很多编程库的时候,网络编程不只要处理数据,还要控制网络通信过程,这些操作很复杂,比如直接汇编网卡、按位填充帧格式、单独处理数据的字节顺序、单独编写接收、解析、发送等逻辑等。后来有了TCP/IP协议栈,人们只需要调用TCP/IP协议栈提供的API就可以发送自己的数据包,人们重点放在了数据,而通信的控制只需要调用TCP/IP协议栈的相关调用。
后来unix将TCP/IP的网络通信统一到文件系统中,并提供一组系统调用,叫做套接字。
套接字首先是内核进程,这个进程叫做套接字抽象层,它管理着套接字接口、套接字数据以及一组TCP/IP协议栈中多种协议的协议调用。
(1)套接字接口:套接字接口负责接收用户进程的套接字调用,当用户进程进行套接字调用,套接字接口负责创建对应的套接字数据、以及进行对应的TCP/IP协议调用
(2)套接字数据:套接字数据负责存储协议信息(TCP还是UDP还是其他协议)、端点信息(本地地址/端口和目的地址/端口)以及数据缓冲区(内核态,跟文件打开后的缓冲区性质相同)。
对于监听套接字,这个数据缓冲区是连接队列(ConnectQueue),以及响应队列(BackQueue);而对于连接套接字,这个数据缓冲区是端口接收缓冲区及端口发送缓冲区。这些数据缓冲区和文件的缓冲区创建的方式相同。
(3)协议调用:当套接字抽象层接收到用户进程的套接字调用,套接字将读取套接字描述符对应的套接字数据,之后根据套接字数据以及套接字调用的数据调用TCP/IP协议栈中的对应协议的协议调用。
可以看出,套接字实际上就是对TCP/IP协议调用的一层封装,套接字系统调用的实际过程是,套接字抽象层根据套接字系统调用,创建相应的套接字数据,或进行具体协议的协议调用。
这里我画了一幅图描述套接字抽象层的工作过程:
图1 套接字抽象层的工作过程
而在整个套接字通信中,操作系统是将套接字抽象层所创建的网络连接及缓冲区当成文件管理,将套接字连接映射成套接字文件,进而分配描述符,管理缓冲区。套接字文件的文件内容则是从TCP/IP协议栈中获取,由缓冲区进行。这样的抽象使得套接字文件相当于连接的两端所共同读写的文件,而连接两端的文件本身更像是一种管道。
这里我画了一幅图来描述套接字文件模型:
图2 套接字文件模型
1.3 同步I/O:BI/O、NI/O
不管是BI/O、NI/O还是I/O复用都是说服务器处理客户端I/O的方式。
BI/O叫做阻塞I/O,一般流程是先创建监听套接字,然后在一个无限循环里调用accept()监听连接,每当accept()正确返回,就创建一个线程执行后续的连接的网络I/O。在默认的情况下,套接字都是阻塞模式的。下图是BI/O的流程图:
图3 winsock BI/O服务器流程图
NI/O叫做非阻塞I/O,winsock的非阻塞I/O跟阻塞I/O类似,可以通过localsocket()设置,之后进行阻塞的套接字调用,其结果可以立刻返回,可以不像BI/O一样的阻塞。
1.4 select
首先是select的定义的介绍:
// 函数声明来自于winsock2.h int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const PTIMEVAL timeout ); /* @param * 1. nfds : winsock2中忽略了这个参数,用0即可 * (winsock的fd_set是链表实现,而unix是bit数组(最多1024个bit)实现的) * 2. readfds:检查读事件的文件描述符 * 3. writefds:检查写事件的文件描述符 * 4. exceptfds:检查异常事件的文件描述符 * 5. timeout:传入一个结构表示阻塞等待时间,期间用户进程阻塞,NULL表示一直阻塞到时间发生 */
select是一种对打开的文件发生的读写及异常等事件进行监听的系统调用。它的功能是分别根据用户传参的读、写及异常监听的三类fd_set集合中存储的文件描述符分别进行读事件、写事件、异常事件的监听和反馈。下面是select的具体工作原理:
用户进程调用select函数后,内核先将用户进程阻塞,进入内核态
(1)读事件监听:在内核中将readfds内文件描述符记录,之后将readfds集合清空并监听read_set集合内映射的文件的读缓冲区,如果有其中有缓冲区可读,则将该缓冲区映射的文件描述符存储到readfds;
(2)写事件监听:在内核中将writefds内文件描述符记录,之后将writefds集合清空并监听writefds集合内映射的文件的读缓冲区,如果有其中有缓冲区可写,则将该缓冲区映射的文件描述符存储到writefds;
(3)异常事件监听:在内核中将exceptfds内文件描述符记录,之后将exceptfds集合清空并监听exceptfds集合内映射的文件的读缓冲区,如果有其中有文件状态异常,则将异常文件映射的文件描述符存储到exceptfds;
之后将监听结果记录到参数中的集合,并将用户进程切换到用户态;
1.5 异步阻塞I/O:基于select的I/O复用
I/O复用模型表示单个线程就可以同时处理多个I/O,常用的I/O复用模型是select模型或选择模型。对于BI/O和NI/O,服务器触发I/O操作要阻塞或不断轮询直到I/O完成;而对于select模型或者其他异步I/O模型,服务器通过一些机制在I/O发生或完成后再触发I/O操作,不需要服务器不断等待或轮询I/O是否完成。select的异步I/O的机制是,将服务器管理的所有套接字连接都交给内核,将所有套接字描述符通过fd_set结构交给内核专门监控I/O变化的select调用。
因为异步I/O可以在发生I/O或I/O完成后再触发I/O操作,这样大大降低了执行I/O操作所占用的执行时间,相当于并发执行I/O和业务逻辑;而同步I/O则相当于串行执行I/O和业务逻辑。这个并发性就是异步I/O相较于同步I/O的区别与优势。下图是我画的同步I/O和select模型的过程与区别:
图4 同步I/O模型与select模型的过程与区别
2 代码及测试
具体的套接字函数的参数和用法这里不进行讲解了,直接上代码:
2.1 代码
// 服务端程序 #include<tchar.h> #include<winsock2.h> #include<ws2tcpip.h> #include<stdlib.h> #include<stdio.h> #pragma comment(lib,"Ws2_32.lib") #define SERV_PORT 8888 #define FD_SETSIZE 512 //连接中的错误处理 short ErrorSockConnect(SOCKET* sockp){ closesocket(*sockp); WSACleanup(); return 1; } //ipv4地址整数(n) to ipv4地址字符串表达(p) char* inet4_ntop(int family,const void * addr, char* str, size_t strlen){ const u_char *paddr = (const u_char*)addr; if(family == AF_INET){ snprintf(str,sizeof(str),"%d.%d.%d.%d",paddr[0],paddr[1],paddr[2],paddr[3]); return str; }else return NULL; } //业务逻辑处理函数 void controller(char* recvbuf, int iResult){ printf("recv : %d bytes received\n : 内容: %s \n", iResult, recvbuf); } int _tmain(int argc, _TCHAR* argv[]){ system("mode con cols=50 lines=50"); WSADATA wsaData; //system("pause"); SOCKET serverSocket = INVALID_SOCKET; SOCKET acceptSocket = INVALID_SOCKET; char recvbuf[1024]; int recvbufLen = 1024; struct sockaddr_in addrClient; int addrLen = sizeof(addrClient); int iResult; //初始化winsock // WSAStartup() if( ( iResult = WSAStartup(MAKEWORD(2,2),&wsaData)) != 0){ printf("WSAStartup failed with error: %d\n", iResult); return 1; }printf("socket : 创建服务端套接字\n"); if( ( serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_IP)) == INVALID_SOCKET){ printf("socket failed with error: %ld\n", WSAGetLastError() ); WSACleanup(); return 1; }printf("服务端开始创建地址\n"); //创建socket地址结构 SOCKADDR_IN addrServ; addrServ.sin_family = AF_INET; addrServ.sin_addr.S_un.S_addr = htonl(0x7f000001); addrServ.sin_port = htons(SERV_PORT); char ipAddrServ[17] = {0}; int portServ = ntohs(addrServ.sin_port); inet4_ntop(AF_INET, &addrServ.sin_addr, ipAddrServ, 16 ); printf(" server ipv4: %s | port: %d |\nbind : 服务端开始绑定地址\n", ipAddrServ, portServ); if( ( iResult = bind(serverSocket,(const struct sockaddr*) &addrServ,addrLen)) == SOCKET_ERROR){ printf("bind failed with error: %d\n", iResult); return ErrorSockConnect(&serverSocket); }printf("listen : 服务端开始监听连接\n"); if( ( iResult = listen(serverSocket,SOMAXCONN)) == SOCKET_ERROR){ printf("listen failed!\n"); return ErrorSockConnect(&serverSocket); }printf(" 服务端TCP开启(io复用模式)\n"); fd_set fdRead, fdSocket; FD_ZERO(&fdSocket); FD_SET(serverSocket, &fdSocket); while(1){ fdRead = fdSocket; if( ( iResult = select(0,&fdRead,NULL, NULL, NULL)) > 0){ //有网络事件发生 // 遍历 描述符集合,确定有哪些套接字有未决IO,之后分别处理掉这些IO u_int i = 0;for(;i < fdSocket.fd_count;i++){ if( FD_ISSET(fdSocket.fd_array[i], &fdRead) ){//当前套接字是否在集合内 if(fdSocket.fd_array[i] == serverSocket){//遍历到 服务端sock printf("\nselect事件:连接到新的客户端\n"); //将等待并建立(accept)新的连接 if(fdSocket.fd_count < FD_SETSIZE){//同时连接数不超过连接限制 if( ( acceptSocket = accept(serverSocket,(struct sockaddr*) &addrClient,&addrLen)) == INVALID_SOCKET){ printf("accept failed\n"); return ErrorSockConnect(&serverSocket); } //增加新的连接套接字, 进行复用等待 FD_SET(acceptSocket, &fdSocket); char ipAddr[17] = {0}; int port = ntohs(addrClient.sin_port); inet4_ntop(AF_INET, &addrClient.sin_addr, ipAddr, 16 ); printf("accept : 连接client ipv4: %s | port: %d |\n", ipAddr, port); }else{//同时连接数超过连接限制 printf("最大连接数:%d ,连接个数超限!\n", FD_SETSIZE); continue; } }else{//遍历到 客户端sock //recv接收客户端的 数据 及 请求 ,并做出 响应 ZeroMemory(recvbuf,recvbufLen); printf("\nselect事件:第%d个客户端",i); getpeername(serverSocket, (struct sockaddr*) &addrClient, &addrLen); char ipAddr[17] = {0}; int port = ntohs(addrClient.sin_port); inet4_ntop(AF_INET, &addrClient.sin_addr, ipAddr, 16 ); printf(":(client ipv4: %s | port: %d )\n", ipAddr, port); if( ( iResult = recv(fdSocket.fd_array[i], recvbuf, recvbufLen, 0) ) > 0){ //成功接收到数据 //业务逻辑 controller(recvbuf, iResult); ZeroMemory(recvbuf, recvbufLen); }else if(iResult == 0){//tcp连接关闭 closesocket(fdSocket.fd_array[i]); FD_CLR(fdSocket.fd_array[i],&fdSocket); printf("--当前TCP连接已关闭--\n"); }else {//接收失败 printf("连接中断,错误类型:%d\n",WSAGetLastError()); closesocket(fdSocket.fd_array[i]); FD_CLR(fdSocket.fd_array[i],&fdSocket); } } } } }else {//select返回值<=0,select失败 printf("select失败,错误类型:\n", WSAGetLastError()); break; } } //关闭套接字,释放资源 closesocket(serverSocket); WSACleanup(); return 0; }
// 客户端程序
#include <tchar.h> #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> #include <conio.h> #pragma comment(lib,"Ws2_32.lib") #pragma comment(lib,"Mswsock.lib") #pragma comment(lib,"AdvApi32.lib") //服务端地址 #define SERVER_IPV4 "127.0.0.1" #define SERVER_PORT "8888" #define RECV_BUFLEN 1024 #define SEND_BUFLEN 1024 //连接中的错误处理 short ErrorSockConnect(SOCKET* sockp){ closesocket(*sockp); WSACleanup(); return 1; } //ipv4地址整数(n) to ipv4地址字符串表达(p) char* inet4_ntop(int family,const void * addr, char* str, size_t strlen){ const u_char *paddr = (const u_char*)addr; if(family == AF_INET){ snprintf(str,sizeof(str),"%d.%d.%d.%d",paddr[0],paddr[1],paddr[2],paddr[3]); return str; }else return NULL; } //带清空用户发送缓冲区操作的send函数 int SEND(SOCKET s,const char *buf,int len,int flags){ int sendResult = send(s, buf, len, flags); ZeroMemory(buf, len); return sendResult; } //////////////////////// int ScanfToSend(SOCKET connectSocket, char* sendbuf, char* recvbuf); //业务逻辑函数 int controller(SOCKET connectSocket, char* sendbuf, char* recvbuf){ printf("user>"); int funcRes = ScanfToSend(connectSocket, sendbuf, recvbuf); return funcRes; } //无阻塞输入函数 void NoBlockInput(char* inputbuf){ int inputNum = 0; while(1){ inputbuf[inputNum] = getch(); if(inputbuf[inputNum] == '\r'){//回车键 inputbuf[inputNum] = 0; printf("\n"); return; } else if(inputbuf[inputNum] == 8){//退格键 if(inputNum > 0){//不是当前输入的首位,可以退格 inputbuf[inputNum--] = 0; inputbuf[inputNum] = 0; printf("\b \b"); } else{//是当前输出的首位,不可以退格 inputbuf[inputNum] = 0; } }else{//正常输入 printf("%c",inputbuf[inputNum++]); } } } int ScanfToSend(SOCKET connectSocket, char* sendbuf, char* recvbuf){ NoBlockInput(sendbuf); int iResult; if(strlen(sendbuf) == 0)return 0; if(!strcmp(sendbuf,"exit")){ if( (iResult = shutdown(connectSocket, SD_SEND)) == SOCKET_ERROR){ printf("shutdown failed with error:%d", iResult); return ErrorSockConnect(&connectSocket); } do{ //shutdown后,接收服务器发来的剩余数据 if( (iResult = recv(connectSocket, recvbuf, sizeof(recvbuf), 0)) == SOCKET_ERROR){ printf("recv failed with error:%d\n", iResult); return ErrorSockConnect(&connectSocket); } }while(iResult > 0); closesocket(connectSocket); WSACleanup(); printf("TCP连接已关闭,客户端即将退出\n"); system("pause"); return 1; } iResult = SEND(connectSocket, sendbuf, (int)strlen(sendbuf), 0); if(iResult == SOCKET_ERROR){ printf("send failed with error: %d\n", WSAGetLastError()); return ErrorSockConnect(&connectSocket); //return 1; } printf("输入发送成功\n"); return 0; } ///////////////////////// int _tmain(int argc,_TCHAR* argv){ system("mode con cols=50 lines=20"); WSADATA wsaData; SOCKET connectSocket = INVALID_SOCKET; struct addrinfo *res = NULL, *ptr = NULL, hints; char sendbuf[SEND_BUFLEN] = {0}, recvbuf[RECV_BUFLEN] = {0}; int iResult; if( (iResult = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0){ printf("WSAStartup failed\n"); return 1; }printf("getaddrinfo : 开始解析服务器ipv4和port\n"); ZeroMemory( &hints, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; //解析服务器地址和端口号 if( ( iResult = getaddrinfo(SERVER_IPV4, SERVER_PORT, &hints, &res) ) != 0 ){ //hints内填写调用者想要获得的addrinfo内容的线索,res是指向addrinfo结构的链表的指针 printf("getaddrinfo failed with error: %d",iResult); WSACleanup(); return 1; }printf("socket(circul) : 尝试连接到服务器,直到成功\n"); int i = 1;for(ptr = res;ptr!=NULL;ptr = ptr->ai_next){ //创建套接字 if( (connectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol )) == INVALID_SOCKET ){ printf("socket failed with error: %d\n",iResult); WSACleanup(); return 1; }printf("向服务器请求连接,当前次数:%d\n", i++); char ipAddr[17] = {0}; struct sockaddr_in* serverAddr = (struct sockaddr_in*)ptr->ai_addr; int port = ntohs(serverAddr->sin_port); inet4_ntop(AF_INET, &(serverAddr->sin_addr), ipAddr, 16 ); printf("server ipv4: %s | port: %d )\n", ipAddr, port); if( (iResult = connect(connectSocket, ptr->ai_addr, (int)ptr->ai_addrlen)) == SOCKET_ERROR){ closesocket(connectSocket); connectSocket = INVALID_SOCKET; continue; }break; }freeaddrinfo(res);//已连接到服务器,释放服务器addrinfo链表 if(connectSocket == INVALID_SOCKET){ printf("连接服务器失败\n"); WSACleanup(); system("pause"); return 1; } //执行业务逻辑 while(1){ if(controller(connectSocket,sendbuf,recvbuf) == 1)return 1; } }
2.2 测试与抓包分析
结果与分析见图5:
图5 select模型的C/S测试与抓包分析
感谢观看,让我们共同进步!
标签:iResult,return,SOCKET,int,编程,printf,接字,winsock,select From: https://www.cnblogs.com/lovecodingforever/p/17023813.html