C++ TCP网络编程--服务器端多线程处理会话连接_xiyangxiaoguo的博客-优快云博客
上一篇采用的是建立新的线程的方法去处理一个新的客户端到服务器的TCP连接,对于少量的客户端连接到服务器这种方法不存在问题,这种方式带来一个问题就是,每一个链接都要开辟一个新的线程,数量少时还可以,当数量上亿时就不合适了,另外,这个很多时候这个链接也没有数据读取,那么这个线程一直运行也会浪费CPU,总之这种方式有局限性。
采取的替代方法是用select实现异步非阻塞的网络连接
Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如 connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等 待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。
Select的函数格式: int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
参数:
nfds:忽略。//windows下可以完全忽略
readfds: 指向检查可读性的套接字集合的可选的指针。
writefds: 指向检查可写性的套接字集合的可选的指针。
exceptfds: 指向检查错误的套接字集合的可选的指针。
timeout: select函数需要等待的最长时间,需要以TIMEVAL结构体格式提供此参数,对于阻塞操作,此参数为null。
参数readfds指示检查套接字的可读性:
(1)当套接字在listen状态,如果已经接收一个连接请求,这个套接字会被标记为可读,此时对该套接字采用一个accept会确保立刻完成,而不被阻塞。
(2)对于其他的套接字,可读性意味着队列中的数据适合读,当调用recv,WSARecv,WSARecvFrom或者recvfrom后不会阻塞。
(3)对于面向连接的套接字,可读性也可以指示关闭套接字的从另一端接收的请求。如果虚电路正常关闭,并且所有的数据都已经接收,然后recv会立刻返回(没有数据接收),如果虚电路重置,recv会立刻返回错误码,例如WSAECONNRESET。如果套接字选项SO_OOBINLINE置位(参见setsockop),出现的OOB数据将会被检查。
参数writefds指示检查套接字的可写性:
(1)如果套接字处理connect调用(非阻塞的),并且完全建立连接,这时套接字是可写。
(2)如果套接字没有处理connect调用,可写性意味着如果此时调用send,sendto或者WSASendto就能立刻执行成功,而不被阻塞。但是,如果len参数超过系统的缓存空间大小,它们在阻塞套接字中是可以阻塞的。不确定多长的长度是合法的,尤其在多线程环境下。
参数exceptfds指示套接字被检查OOB数据出现或者异常错误环境。
注意:OOB数据仅仅应用当SO_OOBINLINE设置为FALSE的情况下。如果一个套接字处理连接调用(非阻塞模式),试图连接的错误信息在exceptfds中,这个文档并没有定义那些错误需要包含其中。
readfd,writefds或者exceptfds中任何两个参数在调用的时候需要为null。至少一个必须为非空,并且任何一个非空描述设置必须包括至少一个套接字句柄。
返回值:
//返回0代表在描述词状态改变已超过timeout时间,返回-1表示错误,返回正整数表示满足条件的套接字个数;
附加说明:这里fd_set 是一种文件描述集合的结构
- FD_SET read_set;//创建文件描述集合
- FD_ZERO(fd_set *set)将一个文件描述符集合清零
- FD_SET(int fd, fd_set *set)将文件描述符fd 加入集合set 中。
- FD_CLR(int fd, fd_set *set)将文件描述符fd 从集合set 中删除.
- FD_ISSET(int fd, fd_set *set)测试文件描述符fd 是否存在于文件描述符set 中.
第一,struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫 无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合 FD_ZERO(fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。
下面给出了一份包含客户端和服务器端的程序,调试没有问题。通过wireshark抓包后查看数据的通信,也没有问题
客户端程序:
#include <winsock.h>
#include<iostream>
#pragma comment(lib,"ws2_32.lib")
#include <Windows.h>
#include<string>
int main()
{
//初始化Windows Socket Application
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
//WinSock的注册函数,初始化底层的Windows Sockets DLL
if (WSAStartup(sockVersion, &wsaData) != 0)
return 0;
//创建一个socket并返回socket的标识符
SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sclient == INVALID_SOCKET)
{
std::cout << "invalid socket!" << std::endl;
return 0;
}
sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8888);
serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
{ //连接失败
std::cout << "connect error !" << std::endl;
closesocket(sclient);
return 0;
}
std::string data="abcdef_";
char* chs = new char[1000];
for (int i = 0; i < 1000; ++i)
chs[i] = i;
const char * sendData = chs;
char recData[255];
int n = 0;
while (1)
{
//data = "abcdef_"+std::to_string(n);
//std::cin >> data;
//sendData = data.c_str();
//string转const char*
/* send()用来将数据由指定的socket传给对方主机
int send(int s, const void * msg, int len, unsigned int flags)
s为已建立好连接的socket,msg指向数据内容,len则为数据长度,参数flags一般设0
成功则返回实际传送出去的字符数,失败返回-1,错误原因存于error */
send(sclient, sendData, 100, 0);//一次发送多,接收少
//接收返回的数据
//int ret = recv(sclient, recData, 255, 0);//阻塞在此处等待接收
//if (ret > 0)
//{
// recData[ret] = 0x00;
// std::cout <<"第"<<n<<"回:"<< recData << std::endl;
//}
if (n++>10)
{
break;
}
Sleep(1000);
}
delete[] chs;
//关闭
closesocket(sclient);
WSACleanup();
system("pause");
return 0;
}
服务器端程序:
#include <winsock.h>
#include<iostream>
#pragma comment(lib,"ws2_32.lib")
#include <Windows.h>
#include <string>
//采用select实现多个连接同时处理
int main()
{
//初始化Windows Socket Application
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
//WinSock的注册函数,初始化底层的Windows Sockets DLL
if (WSAStartup(sockVersion, &wsaData) != 0)
return 0;
//创建服务器端监听套接字
SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (slisten == INVALID_SOCKET)
{
std::cout << "create socket error!" << std::endl;
return 0;
}
//绑定IP和端口
sockaddr_in sin;
sin.sin_family = AF_INET;
//
sin.sin_port = htons(8888);//指定端口,将端口号转换为网络字节顺序
sin.sin_addr.S_un.S_addr = INADDR_ANY;
//bind()把socket绑定到特定的网络地址上
if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
std::cout << "bind error!" << std::endl;
return 0;
}
//开始监听
//启动指定的socket,监听到来的连接请求
//5指定了监听socket的等待连接缓冲区队列的最大长度,一般设为5
if (listen(slisten, 5) == SOCKET_ERROR)
{
std::cout << "listen error !" << std::endl;
return 0;
}
std::cout << "服务器开启成功" << std::endl;
FD_SET read_set;//创建文件描述集合
FD_ZERO(&read_set);//清空文件描述集合
FD_SET(slisten, &read_set);//将监听套接字放入文件描述集合
timeval timeout{0,0};//select超时设置
while (true)
{
FD_SET settmp;
FD_ZERO(&settmp);
settmp = read_set;
//检查文件描述集合中的socket状态是否就绪
int total = select(0, &settmp, nullptr, nullptr, &timeout);//第五参数 nullptr为阻塞模式,时间为0则为非阻塞模式
if (total == SOCKET_ERROR)
{
continue;
}
for (int i = 0; i < settmp.fd_count; i++)
{
SOCKET ss = settmp.fd_array[i];
if (ss == slisten)//监听套接字有就绪
{
循环接收数据
SOCKET sClient;
sockaddr_in remoteAddr;
int nAddrlen = sizeof(remoteAddr);
//接收一个连接请求,并新建一个socket,原来的socket返回监听状态
sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
if (sClient == INVALID_SOCKET)
{
std::cout << "accept error !" << std::endl;
continue;
}
//inet_addr()把一个标准的点分十进制的IP地址转换成长整型的地址数据
//inet_ntoa()把长整型的IP地址数据转换成点分十进制的ASCII字符串
std::cout << "接受一个连接:" << inet_ntoa(remoteAddr.sin_addr) << ":" << remoteAddr.sin_port << std::endl;
FD_SET(sClient, &read_set);
}
else
{
//客户端连接的socket
char msg[10000];
int ret=recv(ss, msg, 10000, 0);//接收数据
if (ret == SOCKET_ERROR || ret == 0)
{
closesocket(ss);
FD_CLR(ss, &read_set);
std::cout << "结束一个连接" << std::endl;
}
else
{
std::cout << "接收到数据" << std::endl;
}
}
}
Sleep(100);
}
closesocket(slisten);
//winsock的注销函数,从底层的Windows Sockets DLL 中撤销注册
WSACleanup();
system("pause");
return 0;
}
下面给出详细分析
(1)打开服务器程序,服务器端监听(监听时不阻塞),服务器程序进入while(true)循环,执行select(),但监听套接字不就绪;
(2)然后打开客户端程序,直到connect()连接,11736端口发送数据包1,前3个数据包建立TCP通信成功
(3)服务器端程序将select()成功返回,即监听套接字就绪,有新的连接加入,accept()该连接,并加入文件描述集合read_set
(4)客户端程序send(100)字节【数据包4】,
(5)服务器端程序select()将成功一次,即11736的端口发来的数据,使得服务器端8888的接收缓存不为空,于是recv(100)字节
接下来的循环内select()将不成功,直到客户区再次send(100)字节【数据包6】,重复(5)
.
.
.
(6)打开第二个客户端程序,直到其connect(),建立TCP连接【数据包10】,数据包10-数据包12建立TCP会话
(7)服务器端程序select()成功,监听套接字就绪,有新的连接加入,accept()该连接,并加入文件描述集合read_set
此时文件描述集合内共有三个socket,服务器的监听套接字以及与两个客户端的会话socket
接下来就是两个客户端分别发送数据到服务器端,从数据包可以清楚看到
直到最后两个客户端分别主动与服务器断开连接,【数据包49-52】【数据包59-62】