28、vSocket模型详解及select应用详解
在上片文章已经讲过了TCP协议的基本结构和构成并举例,也粗略的讲过了SOCKET,但是讲解的并不完善,这里详细讲解下关于SOCKET的编程的I/O复用函数。
1、I/O复用:selec函数
在介绍socket编程之前,首先要熟悉下I/O多路转接技术,尽管SOCKET通信编程有很多模型,但是,在UNIX环境下,使用I/O多路转接模型无疑是一种更好的选择,UNIX下有5种I/0模型,分别是阻塞式I/O.非阻塞式I/O、I/O复用(select和poll)、信号驱动式I/O,异步I/O。这5种方式都可用SOCKET编程,这里只介绍阻塞式I/O和I/O复用,如果向详细了解I/O模型的,可以参考《UNIX网络编程卷一:套接字联网API》,或着查看
Socket模型详解(转)
。
(1)阻塞式I/O
阻塞式I/O很好理解,一个线程利用recvfrom系统接收来自一个socket上的数据,没有数据的时候就等待,一直等到有数据,将数据交给应用去处理,之前降到的connect、accept、recv、recvfrom都是属于阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。
(2)I/O复用
对于只监控一个socket的应用程序来说,阻塞式I/O模型会工作的很好,但是如果需要监控多个socket的话,阻塞式I/O处理起来就比较麻烦了,这时候就可以使用I/O复用很好的解决这个问题。使用I/O复用时,所有的程序将需要监控的socket交给select函数,当某个socket上有可用数据时,select函数会返回通知应用程序。
(3)select函数
1、select函数原型(UNIX中提供POLL函数与之类似)
select函数允许进程指示内核等待多个事件中的任何一个发生,并只在一个或多个事件发生或经历一段指定时间后才唤醒它。其原型如下:
int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *esceptfds, const struct timeval *timeout);
2、select函数参数
第一个参数是需要监控的描述符个数,他的值是最大描述符加1。中间三个参数是要让内核测试读、写和异常条件的描述符,参数readfds指定了被读监控的文件描述集;参数writefds指定了被写监控的文件描述符集,而参数exceptfds被指定了异常监控的文件描述符集,如果我们对某一个条件不感兴趣,就可以设置为空指针。
这里异常条件中包含了TCP的带外数据到达,大多数嵌入的API返回一个int返回代码。如果该码是<0,则一个错误已发生。错误的性质可以利用所谓的“错误号”,然而,在没有全局变量的多任务工作 环境中,socket领域,我们可以带哦用以下代码来查询错误。
int espx_last_socket_errno(int socket) { int ret = 0; u32_t optlen = sizeof(ret); getsockopt(socket, SOL_SOCKET, SO_ERROR, &ret, &optlen); return ret; }
参数timeout起到了定时器的作用,到来指定的时间,无论是否有设备准备好,都返回调用,timeval的结构定义如下:
struct timeval { long tv_sec;//秒 long tv_usec;//微秒 };
timeout 取不同值时,该调用就表现不同的性质:
1)、timeout为0,即两个成员的取值必须为0;select调用立即返回,这样就类似为轮询。
2)、timeout为NULL,select()调用就阻塞,也就是永远等待,直到有描述符就绪。
3)、timeout为整数,就是等待一段时间,在这段时间内如果有描述符准备就绪,即立即返回,如果超过时间不管有没有准备符也立即返回。
对于这个参数在UNIX系统下的使用值得注意的是:当timeout设置为永远等待或者等待一段时间的模式时,select函数可能会被信号中断,有些unix的内核在这种情况下,不会再次重启select,因此程序中应该要有处理select返回EINTR错误的准备。
另外,尽管timeout参数可以表示很大的数值,但部分系统不一定会至此,可能会返回错误。
如果,select函数中间三个参数都为空的话,select就成为了一个比sleep更为精确的休眠函数。
3、select函数返回值
select函数调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writerfds、和exceptfds中的没有就绪的描述符。这个情况是值得注意的,在一个典型的socket接收数据的程序中,当select检测到有数据过来,select函数返回,称故乡继续处理接收到的数据,数据处理结束,然后继续调用select监测,这个时候应该使用FD系列(windows和UNIX下)宏重新设置描述符集,在Solaris下,不重新设置的话,有时还能正常工作,但是在部分linux下就不会正常工作。
select的返回值有如下情况:
1)正常情况下返回就绪的文件描述符个数。
2)经历过timeout时长后仍无设备准备好,返回0;
3)、如果select被某个信号中断,他将返回-1并设置error为EINTR.
4)如果出错,返回-1并设置相应的errno;
2、FD系列宏
系统提供了4个宏对描述符集进行操作:
1)void FD_SET(int fd,fd_set *fdset);//宏FD_SET设置文件描述符集fdset中对应的fd的位(设置为1)
2)void FD_CLR(int fd,fd_set *fdset);//宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为0)
3)void FD_ISSET(int fd,fd_set *fdset);//宏FD_ZERO清除文件描述符集fdset中的所有位(即把所有位都设置为0)
4)void FD_ZERO(fd_set *fdset);//检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。
前三个宏在调用select前被描述符屏蔽位,在调用select后使用FD_ISSET进行检测。
3、select函数应用举例
1、通过前面的学习,我们可以编程如下来首先完成一个客户端的连接:
/* 文件名称:socket.c 功能说明:通过selct实现TCP服务器的非阻塞模式 使用说明:复制此文件,对argc赋值2,argv[1]先赋值1运行启动服务器,再赋值2启动客户端发送数据 编写日期:2017-11-18 修改历史:无 */ //包含文件申明 #include <stdio.h> #include<io.h> #include <string.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") //全局变量定义初始化 #define S_ADDR S_un.S_addr #define CLOSE closesocket struct socketaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; /************************************************************ 函数名称:socket_init 函数功能:打开windows下的socket,UNIX下不用 参数: 数据类型 输入/输出描述 说明:socket设置失败返回1,socket设置成功返回0 ************************************************************/ void socket_init()//为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。 { WSADATA wsa; printf("\n初始化中Initialising Winsock...\n"); if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { printf("Failed. Error Code : %d", WSAGetLastError()); return 1; } printf("初始化成功Initialised.\n"); } /************************************************************ 函数名称:tcp_client 函数功能:创建一个tcp客户端,给IP为"192.168.191.1"的服务器7777端口发送数据Hi,TCP 参数: 数据类型 输入/输出描述 说明:IP地址0"192.168.191.1"是本机现在的网络环境,不同的网络环境的IP不同。可用IPCONFIG在windows的cmd下查询 ************************************************************/ void tcp_client() { int sockfd; struct sockaddr_in ServAddr; int ret; char* SendStr = "Hi,Tcp"; //定义一个发送的指针,指针指向要发送的数据的地址 int len; sockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数 ServAddr.sin_family = AF_INET; ServAddr.sin_port = htons(7777); ServAddr.sin_addr.S_ADDR = inet_addr("192.168.191.1");//配置ServAddr结构体参数 ret = connect(sockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr));//connect有TCP客户端用来和TCP服务器建立连接的 if (ret != 0) { return; } len = send(sockfd, SendStr, strlen(SendStr), 0); //发送数据到服务器 if (len <= 0) { printf("send data error\n"); } CLOSE(sockfd);//关闭socket函数 return; } /************************************************************ 函数名称:tcp_server 函数功能:创建一个tcp服务器,接收任何IP地址的客户端给其7777端口发送的数据并显示 参数: 数据类型 输入/输出描述 说明:无 ************************************************************/ void tcp_server() { int listensockfd; int connfd; struct sockaddr_in ServAddr; fd_set RecvdFd; struct timeval timeout; int ret; char RecvBuf[64]; int len; struct sockaddr ConnAddr; int ConnAddrLen = sizeof(struct sockaddr); int maxfd; listensockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数 maxfd = listensockfd; ServAddr.sin_family = AF_INET; ServAddr.sin_port = htons(7777); ServAddr.sin_addr.S_ADDR = htonl(INADDR_ANY);//配置ServAddr数据,所有iP的设备都可以通过7777端口将数据传输到这个服务器 // if (bind(listensockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr)) != 0)// //将本地的一个IP地址和套接字绑定在一起 { printf("bind error\n"); return; } if (listen(listensockfd, 1) != 0) //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手 { printf("listen error\n "); return; } timeout.tv_sec = 60; timeout.tv_usec = 0; FD_ZERO(&RecvdFd); //清除监听select函数描述符的监听符 for (;;) { FD_SET(listensockfd, &RecvdFd); //设置文件描述符集,将监听socket的描述符加入到select的监控中 memset(RecvBuf, 0, sizeof(RecvBuf)); //分配接受数据的内存 ret = select(maxfd + 1, &RecvdFd, NULL, NULL, &timeout); //配置select函数监控设定的描述符,并且只对“读”事件关心 if (-1 == ret) { continue; } if (FD_ISSET(listensockfd, &RecvdFd)) //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生 // { connfd = accept(listensockfd, &ConnAddr, &ConnAddrLen); //如果是,则调用accept函数进行事件处理, if (-1 == connfd) { printf("accept error\n"); continue; } FD_SET(connfd, &RecvdFd); //将accept返回的客户连接描述符加入到select监控位 } if (FD_ISSET(connfd, &RecvdFd)) //select函数返回,通知有事件,判断是否是客户连接成功的描述符上有事件发生 { len = recv(connfd, RecvBuf, 6, 0); //如果是,调用recv函数进行数据接收 if (len <= 0) //如果recv接受长度小于0,则表示对端关闭 { FD_CLR(connfd, &RecvdFd); CLOSE(connfd); //调用socket关闭函数,关闭本端socket continue; } printf("%s\n", RecvBuf); //将接收的数据显示出来 } } return; } /************************************************************ 函数名称:main 函数功能:主函数 参数: 数据类型 输入/输出描述 argc int 输入的数据argc argv char* 输入的数组argv[] 说明:argc=2,且argv[1]为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2 ************************************************************/ void main(int argc, char* argv[]) { argc = 2;//是2是为了保证我们设定参数 argv[1] = "2";//为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2 int type; socket_init(); if (argc != 2) { printf("Usage:< s% [Mode 1|2]>\n", argv[0]); return; } type = strtol(argv[1], NULL, 10); if (1 == type) { printf("Server Mode Run!\n "); tcp_server(); return; } if (2 == type) { printf("Client Mode Run!\n"); tcp_client(); return; } return; }
实验现象:
先运行argv[1]赋值为1的程序
再运行argv[1]赋值为2的程序,可以看到如下结果
可以看到数据发送成功。
2、为了体现select函数的优越性,将程序进行修改,让客户端发送一系列字符串,服务器端将字符串打印出来,并且让其同时处理两个客户端连接。
/* 文件名称:socket.c 功能说明:通过selct实现TCP服务器的非阻塞模式 使用说明:复制此文件,对argc赋值2,argv[1]先赋值1运行启动服务器,再赋值2启动客户端发送数据 编写日期:2017-11-18 修改历史: 2017-11-18 修改发送数据,使client发送字符串,优化服务器,使服务器可处理多个客户端数据 */ //包含文件申明 #include <stdio.h> #include<io.h> #include <string.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") //全局变量定义初始化 #define S_ADDR S_un.S_addr #define CLOSE closesocket struct socketaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; /************************************************************ 函数名称:socket_init 函数功能:打开windows下的socket,UNIX下不用 参数: 数据类型 输入/输出描述 说明:socket设置失败返回1,socket设置成功返回0 ************************************************************/ void socket_init()//为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。 { WSADATA wsa; printf("\n初始化中Initialising Winsock...\n"); if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { printf("Failed. Error Code : %d", WSAGetLastError()); return 1; } printf("初始化成功Initialised.\n"); } /************************************************************ 函数名称:tcp_client 函数功能:创建一个tcp客户端,给IP为"192.168.191.1"的服务器7777端口发送数据Hi,TCP 参数: 数据类型 输入/输出描述 说明:IP地址0"192.168.191.1"是本机现在的网络环境,不同的网络环境的IP不同。可用IPCONFIG在windows的cmd下查询 修改历史: 2017-11-18 将字符发送优化为字符串发送 ************************************************************/ void tcp_client() { int sockfd; struct sockaddr_in ServAddr; int ret; char* SendStr[] = { "Hi,Tcp", "HI,TCP,nice to meet you", "HI,TCP,are you ok???" };//定义一个发送的指针,指针指向要发送的数据的地址 unsigned int SendStrlen = 0; unsigned int SendLen = 0; int loop = 0; int len; sockfd = socket(AF_INET, SOCK_STREAM, 0); ServAddr.sin_family = AF_INET; ServAddr.sin_port = htons(7777); ServAddr.sin_addr.S_ADDR = inet_addr("192.168.191.1");//配置ServAddr结构体参数 ret = connect(sockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr));//connect有TCP客户端用来和TCP服务器建立连接的 if (ret != 0) { return; } for (loop = 0; loop < 5; loop++) { SendStrlen = strlen(SendStr[loop]); SendLen = htonl(SendStrlen); len = send(sockfd, (char*)&SendLen, sizeof(SendLen), 0); if (len <= 0) { printf("send data error\n"); break; } len = send(sockfd, SendStr[loop], SendStrlen, 0); if (len <= 0) { printf("send data error\n"); break; } } CLOSE(sockfd);//关闭socket函数 return; } /************************************************************ 函数名称:tcp_server 函数功能:创建一个tcp服务器,接收任何IP地址的客户端给其7777端口发送的数据并显示 参数: 数据类型 输入/输出描述 说明:无 ************************************************************/ void tcp_server() { int listensockfd; int connfd; struct sockaddr_in ServAddr; fd_set RecvdFd; struct timeval timeout; int ret; char RecvBuf[64]; int len; struct sockaddr ConnAddr; int ConnAddrLen = sizeof(struct sockaddr); int maxfd; int maxconn = 2; int clientfd[2] = { -1,-1 }; int loop = 0; unsigned int RecvStrlen = 0; unsigned int RecvedTotallen = 0; listensockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数 maxfd = listensockfd; ServAddr.sin_family = AF_INET; ServAddr.sin_port = htons(7777); ServAddr.sin_addr.S_ADDR = htonl(INADDR_ANY);//配置ServAddr数据,所有iP的设备都可以通过7777端口将数据传输到这个服务器 // if (bind(listensockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr)) != 0)// //将本地的一个IP地址和套接字绑定在一起 { printf("bind error\n"); return; } if (listen(listensockfd, 1) != 0) //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手 { printf("listen error\n "); return; } timeout.tv_sec = 60; timeout.tv_usec = 0; FD_ZERO(&RecvdFd); //清除监听select函数描述符的监听符 for (;;) { FD_SET(listensockfd, &RecvdFd); //设置文件描述符集,将监听socket的描述符加入到select的监控中 memset(RecvBuf, 0, sizeof(RecvBuf)); //分配接受数据的内存 ret = select(maxfd + 1, &RecvdFd, NULL, NULL, &timeout); //配置select函数监控设定的描述符,并且只对“读”事件关心 if (-1 == ret) { continue; } if (FD_ISSET(listensockfd, &RecvdFd)) //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生 { connfd = accept(listensockfd, &ConnAddr, &ConnAddrLen); //如果是,则调用accept函数进行事件处理, if (-1 == connfd) { printf("accept error=%d\n", GetLastError()); continue; } for (loop = 0; loop < 2; loop++)//最多允许两个客户输入,并将客户连接的socket描述符保存好。 { if (-1 == clientfd[loop]) { FD_SET(connfd, &RecvdFd); //将accept返回的客户连接描述符加入到select监控位 clientfd[loop] = connfd; maxfd = (maxfd > connfd ? maxfd : connfd); break; } } if (loop >= 2) { printf("Max connect reached.\n"); } } for (loop = 0; loop < 2; loop++)//循环检查来两个客户端的socket描述符,看是否有数据可读。 { if (FD_ISSET(clientfd[loop], &RecvdFd)) //select函数返回,通知有事件,判断是否是客户连接成功的描述符上有事件发生 { RecvStrlen = 0; len = recv(connfd, (char*)&RecvStrlen, sizeof(RecvStrlen), 0); //如果是,调用recv函数进行数据接收 if (len <= 0) //如果recv接受长度小于0,则表示对端关闭 { FD_CLR(connfd, &RecvdFd); CLOSE(connfd); //调用socket关闭函数,关闭本端socket continue;//先接收四个字节长度的字符串长度 } memset(RecvBuf, 0, sizeof(RecvBuf)); RecvStrlen = ntohl(RecvStrlen);//将接收到的长度进行字节转换,转换成本机序 RecvedTotallen = 0; while (1)//循环接收每次发送的字符串,直到制定长度 { len = recv(connfd, &RecvBuf[RecvedTotallen], RecvStrlen, 0); if (len <= 0) { FD_CLR(clientfd[loop], &RecvdFd); closesocket(clientfd[loop]); break; } RecvedTotallen += len; if (RecvedTotallen == RecvStrlen) { break; } } printf("%s\n", RecvBuf); } } } return; } /************************************************************ 函数名称:main 函数功能:主函数 参数: 数据类型 输入/输出描述 argc int 输入的数据argc argv char* 输入的数组argv[] 说明:argc=2,且argv[1]为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2 ************************************************************/ void main(int argc, char* argv[]) { argc = 2;//是2是为了保证我们设定参数 argv[1] = "1";//为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2 int type; socket_init(); if (argc != 2) { printf("Usage:< s% [Mode 1|2]>\n", argv[0]); return; } type = strtol(argv[1], NULL, 10); if (1 == type) { printf("Server Mode Run!\n "); tcp_server(); return; } if (2 == type) { printf("Client Mode Run!\n"); tcp_client(); return; } return; }
同样的方法进行调用结果如下:
当第二个客户端连接到服务器时,有如下输出
3、通过之前的编程和优化,基本上已经实现了大多数TCP服务的应用场景,但是此处还有一个问题,就是当服务器正在接收数据时,另一个客户端又发来数据了该如何处理,这里就设计到TCP的并发处理了,每当accept一个连接时就创建一个线程去单独处理这个连接,下面对前面的程序进行修改如下
/* 文件名称:socket.c 功能说明:通过selct实现TCP服务器的非阻塞模式 使用说明:复制此文件,对argc赋值2,argv[1]先赋值1运行启动服务器,再赋值2启动客户端发送数据 编写日期:2017-11-18 修改历史: 2017-11-18 修改发送数据,使client发送字符串,优化服务器,使服务器可处理多个客户端数据 */ //包含文件申明 #include <stdio.h> #include<io.h> #include <string.h> #include <WinSock2.h> #include <conio.h> #include <process.h> #pragma comment(lib, "ws2_32.lib") //全局变量定义初始化 #define S_ADDR S_un.S_addr #define CLOSE closesocket struct socketaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; /************************************************************ 函数名称:socket_init 函数功能:打开windows下的socket,UNIX下不用 参数: 数据类型 输入/输出描述 说明:socket设置失败返回1,socket设置成功返回0 ************************************************************/ void socket_init()//为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。 { WSADATA wsa; printf("\n初始化中Initialising Winsock...\n"); if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { printf("Failed. Error Code : %d", WSAGetLastError()); return 1; } printf("初始化成功Initialised.\n"); } /************************************************************ 函数名称:tcp_client 函数功能:创建一个tcp客户端,给IP为"192.168.191.1"的服务器7777端口发送数据Hi,TCP 参数: 数据类型 输入/输出描述 说明:IP地址0"192.168.191.1"是本机现在的网络环境,不同的网络环境的IP不同。可用IPCONFIG在windows的cmd下查询 修改历史: 2017-11-18 将字符发送优化为字符串发送 ************************************************************/ void tcp_client() { int sockfd; struct sockaddr_in ServAddr; int ret; char* SendStr[] = { "Hi,Tcp", "HI,TCP,nice to meet you", "HI,TCP,are you ok???" };//定义一个发送的指针,指针指向要发送的数据的地址 unsigned int SendStrlen = 0; unsigned int SendLen = 0; int loop = 0; int len; sockfd = socket(AF_INET, SOCK_STREAM, 0); ServAddr.sin_family = AF_INET; ServAddr.sin_port = htons(7777); ServAddr.sin_addr.S_ADDR = inet_addr("192.168.191.1");//配置ServAddr结构体参数 ret = connect(sockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr));//connect有TCP客户端用来和TCP服务器建立连接的 if (ret != 0) { return; } for (loop = 0; loop < 5; loop++) { SendStrlen = strlen(SendStr[loop]); SendLen = htonl(SendStrlen); len = send(sockfd, (char*)&SendLen, sizeof(SendLen), 0); if (len <= 0) { printf("send data error\n"); break; } len = send(sockfd, SendStr[loop], SendStrlen, 0); if (len <= 0) { printf("send data error\n"); break; } } CLOSE(sockfd);//关闭socket函数 return; } /************************************************************ 函数名称:tcp_server 函数功能:创建一个tcp服务器,接收任何IP地址的客户端给其7777端口发送的数据并显示 参数: 数据类型 输入/输出描述 说明:无 ************************************************************/ void tcp_server() { int listensockfd; int connfd; struct sockaddr_in ServAddr; fd_set RecvdFd; struct timeval timeout; int ret; char RecvBuf[64]; int len; struct sockaddr ConnAddr; int ConnAddrLen = sizeof(struct sockaddr); listensockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数 ServAddr.sin_family = AF_INET; ServAddr.sin_port = htons(7777); ServAddr.sin_addr.S_ADDR = htonl(INADDR_ANY);//配置ServAddr数据,所有iP的设备都可以通过7777端口将数据传输到这个服务器 // if (bind(listensockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr)) != 0)// //将本地的一个IP地址和套接字绑定在一起 { printf("bind error\n"); return; } if (listen(listensockfd, 1) != 0) //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手 { printf("listen error\n "); return; } timeout.tv_sec = 60; timeout.tv_usec = 0; FD_ZERO(&RecvdFd); //清除监听select函数描述符的监听符 for (;;) { FD_SET(listensockfd, &RecvdFd); //设置文件描述符集,将监听socket的描述符加入到select的监控中 ret = select(listensockfd + 1, &RecvdFd, NULL, NULL, &timeout); //配置select函数监控设定的描述符,并且只对“读”事件关心,只监控监听描述符,不再监控客户连接进来的描述符。 if (-1 == ret) { continue; } if (FD_ISSET(listensockfd, &RecvdFd)) //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生 { connfd = accept(listensockfd, &ConnAddr, &ConnAddrLen); //如果是,则调用accept函数进行事件处理, if (-1 == connfd) { printf("accept error=%d\n", GetLastError()); continue; } /*创建线程*/ tcp_create_thread(connfd); } } return; } unsigned int _stdcall tcp_conn_process_thread(void *args)//window下用这个定义 //unix下定义为void* tcp_conn_process_thread(void* args) { int connfd; fd_set RecvdFd; struct timeval timeout; char RecvBuf[64]; unsigned int RecvStrlen = 0; unsigned int RecvedTotallen = 0; int ret; int len; timeout.tv_sec = 60; timeout.tv_usec = 0; connfd = *((int*)args); FD_ZERO(&RecvdFd); //清除监听select函数描述符的监听符 for (;;) { FD_SET(connfd, &RecvdFd); //设置文件描述符集,将监听socket的描述符加入到select的监控中 memset(RecvBuf, 0, sizeof(RecvBuf)); //分配接受数据的内存 ret = select(connfd + 1, &RecvdFd, NULL, NULL, &timeout); //配置select函数监控设定的描述符,并且只对“读”事件关心 if (-1 == ret) { continue; } if (FD_ISSET(connfd, &RecvdFd)) //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生 { RecvStrlen = 0; len = recv(connfd, (char*)&RecvStrlen, sizeof(RecvStrlen), 0); //如果是,调用recv函数进行数据接收 if (len <= 0) //如果recv接受长度小于0,则表示对端关闭 { CLOSE(connfd); //调用socket关闭函数,关闭本端socket break; } memset(RecvBuf, 0, sizeof(RecvBuf)); RecvStrlen = ntohl(RecvStrlen);//将接收到的长度进行字节转换,转换成本机序 RecvedTotallen = 0; while (1)//循环接收每次发送的字符串,直到制定长度 { len = recv(connfd, &RecvBuf[RecvedTotallen], RecvStrlen, 0); if (len <= 0) { CLOSE(connfd); break; } RecvedTotallen += len; if (RecvedTotallen == RecvStrlen) { break; } } printf("%s\n", RecvBuf); } } return 0; } int g_connfd; int tcp_create_thread(int sockfd) { g_connfd = sockfd; //声明一个全局变量,作为参数传递给线程,如果使用局部变量的话,由于主线程与新创线程运行的时间关系,在线创建线程需要 //使用参数地址时,主线程函数调用已经推出,从而导致传递的局部变量的地址已经被释放。 unsigned long threadId; threadId = _beginthreadex(NULL, 0, (unsigned(_stdcall*)(void*))tcp_conn_process_thread, &g_connfd, 0, NULL); //windows下创建一个线程 /*********************************************************************** UNIX下: pthread_t threadId; pthread_create(&threaId,NULL,tcp_conn_process_thread,(void*)&g_connfd); ***********************************************************************/ return; } /************************************************************ 函数名称:main 函数功能:主函数 参数: 数据类型 输入/输出描述 argc int 输入的数据argc argv char* 输入的数组argv[] 说明:argc=2,且argv[1]为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2 ************************************************************/ void main(int argc, char* argv[]) { argc = 2;//是2是为了保证我们设定参数 argv[1] = "1";//为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2 int type; socket_init(); if (argc != 2) { printf("Usage:< s% [Mode 1|2]>\n", argv[0]); return; } type = strtol(argv[1], NULL, 10); if (1 == type) { printf("Server Mode Run!\n "); tcp_server(); return; } if (2 == type) { printf("Client Mode Run!\n"); tcp_client(); return; } return; }
运行程序1、2后结果如下:
到这里,一个功能完善的TCP服务器就搭建完成了。
下一篇文章:
基于visual studio的UDP编程