文章目录
1. 预备知识
1.1 SELECT模型介绍
Select模型是Windows Sockets中最常见的I/O模型。之所以称其为Select模型,是因为的核心是利用select()函数实现I/O管理。利用select()函数,Windows Sockets应用程序可以判断套接字上是否存在数据,或者能否向该套接字写入数据。
如图所示,在调用recv()函数接收数据之前,先调用select()函数。如果此时系统没有可读的数据,那么select()函数会阻塞在这里。当系统存在可读的数据时,该函数返回。此时应用程序就可以调用recv()函数接收数据了。
1.2 select函数
结构:
int WSAAPI select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const timeval *timeout
);
参数说明:
- nfds 一般设置为0,可以忽略,主要是为了兼容其他系统参数兼容。
- readfds 准备接收数据的套接字集合,即可读性集合。
- writefds 准备发送数据的套接字集合,即可写性集合。
- exceptfds 检查错误套接字集合指针。
- timeout 等待时间,设置为NULL时,表示永久等待,直到有事件发生返回。
函数说明:
当程序执行select函数时,程序被阻塞,直至内核检测到有可读可写等套接字时才返回,并修改fd_set集合中数据,这些的数据都是可读可写socket集合,不存在的或没有完成IO操作的套接字会被“删除”,返回值是这些可读可写集合的数量。
若设置超时,则超时时间达到后,函数返回值为0。
需要说明的是,select函数三个套接字指针集合,至少需要传入一个集合才可以 。
1.3 fd_set操作函数
windows sockets提供了下列宏,用来对fd_set进行一系列操作。使用以下宏可以使编程工作简化。
- FD_CLR(s,set);从set集合中删除s套接字。
- FD_ISSET(s,set);检查s是否为set集合的成员。
- FD_SET(s,set);将套接字加入到set集合中。
- FD_ZERO(set);将set集合初始化为空集合。
1.4 select函数与宏的搭配使用
可通过以下步骤,来完成对套接字的可读可写判断。
- 使用FD_ZERO初始化套接字集合。如FD_ZERO(&readfds);
- 使用FD_SET将某套接字放到readfds内,用于select检测,如: FD_SET(s,&readfds);
- 以readfds为第二个参数调用select函数。select在返回时会返回所有fd_set集合中套接字的总个数,并对每个集合进行相应的更新。将满足条件的套接字放在相应的集合中。
- 使用FD_ISSET判断s是否还在某个集合中。如: FD_ISSET(s,&readfds);
- 调用相应的Windows socket api 对某套接字进行操作。
2. 关键代码
2.1 服务器端
- 在套接字处于监听状态后,使用select模型
//在套接字处于监听状态后,使用select模型
FD_SET socketSet;//服务器套接字集合
FD_SET writeSet;//可写套接字集合
FD_SET readSet;//可读套接字集合
FD_ZERO(&socketSet);//初始化套接字集合,即清空集合
FD_SET(listenSocket, &socketSet);//加入监听套接字
- 检测可读套接字,调用检查套接字状态
FD_ZERO(&writeSet);//清空可读套接字集合
FD_ZERO(&readSet);//清空可写套接字集合
readSet = socketSet;//赋值
writeSet = socketSet;
//只检测可读套接字,该函数返回处于就绪状态且已包含在FD_SET结构中的套接字总数
ret = select(0, &readSet, &writeSet, NULL, NULL);
if (SOCKET_ERROR == ret) {
//调用select()失败处理
cout << "select() returned with error:" << ::WSAGetLastError() << endl;
break;
}
- 判断是否存在客户端的连接请求
//判断是否存在客户端的连接请求
if (FD_ISSET(listenSocket, &readSet)) {
SOCKADDR_IN ClientAddr;//保存客户端IP地址端口
int nLen = sizeof(ClientAddr);
//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
acceptSocket = accept(listenSocket, (sockaddr*)&ClientAddr, &nLen);
if (INVALID_SOCKET == acceptSocket) {
cout<<"accept() returned with error:" << ::WSAGetLastError() << endl;
continue;
}
else {
//将该套接字加入服务器套接字结合
FD_SET(acceptSocket, &socketSet);
}
char* pszClientIP = inet_ntoa(ClientAddr.sin_addr); //返回点分十进制的字符串在静态内存中的指针
if (NULL != pszClientIP)
{
//ntohs主要是将网络字节转为主机字节
cout << "客户端[" << pszClientIP << ":" << ntohs(ClientAddr.sin_port) <<"]请求连接成功"<< endl;
cout << "目前客户端的数量为:" << (socketSet.fd_count - 1) << endl;
//等待其他客户端连接
Sleep(1000);
}
cout << endl;
continue;
}
- 遍历所有套接字,判断可读或可写
//遍历所有套接字
for (int i = 1; i < socketSet.fd_count; i++) {
SOCKET sAccept = socketSet.fd_array[i];//获取套接字
SOCKADDR_IN addrClient;
int nLen = sizeof(addrClient);
//获取当前连接的客户端的IP地址和端口号,即初始化addClient
getpeername(sAccept, (sockaddr*)&addrClient, &nLen);
//获取客户端地址以及它的主机字节
char* pszClientIp = inet_ntoa(addrClient.sin_addr);
unsigned short usClientPort = ntohs(addrClient.sin_port);
//该套接字可读
if (FD_ISSET(sAccept, &readSet)) {
char buf[6400] = { '\0' };
ret = recv(sAccept, buf, sizeof(buf), 0);
if (ret > 0)
{
cout << "-------------接收消息-------------" << endl;
cout << "是否显示客户端[" << pszClientIp << ":" << usClientPort << "]的消息及长度?(用1表示是,其他表示否)" << endl;
int n = 0;
cin >> n;
if (n == 1) {
cout << "消息长度为:" << ret << endl;
cout << "消息内容为:" << buf << endl << endl;
}
else {
cout << "不显示消息" << endl;
}
}else if (ret == 0) {
//对方关闭连接
cout << "客户端[" << pszClientIp << ":" << usClientPort << "]主动关闭连接" << endl;
closesocket(sAccept);
FD_CLR(sAccept, &socketSet);
cout << "目前客户端数量为:" << socketSet.fd_count - 1 << endl;
continue;
}
else {
// 客户端的socket没有被正常关闭,即没有调用closesocket
if (::WSAGetLastError() == WSAECONNRESET)
{
cout << "客户端[" << pszClientIp << ":" << usClientPort << "]被强行关闭" << endl;
}
else
{
cout << "recv data error:" << ::WSAGetLastError() << endl;
}
closesocket(sAccept);
//FD_CLR(s,set);从set集合中删除s套接字
FD_CLR(sAccept, &socketSet);
//监听socket不算客户端
printf("目前客户端的数量为: %d\n", socketSet.fd_count - 1);
continue;
}
}
//该套接字可写
if (FD_ISSET(sAccept, &writeSet)) {
t++;
if (t < socketSet.fd_count) {
char buf1[] = { "Hello,I am Server" };
cout << "-------------发送消息-------------" << endl;
cout << "是否向客户端[" << pszClientIp << ":" << usClientPort << "]发送消息(用1表示是,其他表示否)" << endl;
int n = 0;
cin >> n;
if (n == 1) {
ret = send(sAccept, buf1, sizeof(buf1), 0);
cout << "发送信息大小为:" << ret << endl;
cout << "信息内容为:" << buf1 << endl;
cout << endl;
}
}
}
}
}
2.2 客户端
设置三个客户端,即Client1,Client2,Client3
只显示一个,其余两个同理
//4、接收消息
char buf2[6400]={ '\0' };
ret = recv(s, buf2, sizeof(buf2), 0);
cout << "接收信息大小为:" << ret << endl;
cout << "信息内容为:" << buf2 << endl;
//5.关闭套接字(判断)
cout << "是否关闭套接字?(1表示是,其他表示否)" << endl;
int n;
cin >> n;
if (n == 1) {
closesocket(s);
}
2.3 完整代码
select_Server.cpp
#include <winsock2.h>
#include <iostream>
using namespace std;
//指定动态库的lib文件
#pragma comment(lib,"ws2_32.lib")
int main() {
WSADATA wsd;//WSADATA变量
SOCKET listenSocket;//服务器监听套接字
SOCKET acceptSocket;//接受客户端连接请求的套接字
int ret;//返回值
int t = 0;//计数
//初始化套接字动态库
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {
cout << "WSAStartup error:"<<::WSAGetLastError()<<endl;
return 1;
}
//创建套接字
listenSocket= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == listenSocket) {
cout << "create socket error:" << ::WSAGetLastError() << endl;
WSACleanup();//释放套接字资源
return -1;
}
//绑定套接字
sockaddr_in addr;//服务器地址
addr.sin_port = htons(8000);//网络字节序,设置端口号为8000
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络字节序,服务器ip地址
addr.sin_family = AF_INET;//地址族
ret= bind(listenSocket, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == ret) {
cout<<"Bind error:"<< ::WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
//监听套接字
ret= listen(listenSocket, SOMAXCONN);//系统中每一个端口最大的监听队列的长度,默认值为1024
if (SOCKET_ERROR == ret)
{
cout<< "Listen error:" << ::WSAGetLastError() << endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
//在套接字处于监听状态后,使用select模型
FD_SET socketSet;//服务器套接字集合
FD_SET writeSet;//可写套接字集合
FD_SET readSet;//可读套接字集合
FD_ZERO(&socketSet);//初始化套接字集合,即清空集合
FD_SET(listenSocket, &socketSet);//加入监听套接字
cout << "服务器启动监听..." << endl;
//等待套接字满足可读可写条件,在此之前需不断循环
while (true) {
FD_ZERO(&writeSet);//清空可读套接字集合
FD_ZERO(&readSet);//清空可写套接字集合
readSet = socketSet;//赋值
writeSet = socketSet;
//只检测可读套接字,该函数返回处于就绪状态且已包含在FD_SET结构中的套接字总数
ret = select(0, &readSet, &writeSet, NULL, NULL);
if (SOCKET_ERROR == ret) {
//调用select()失败处理
cout << "select() returned with error:" << ::WSAGetLastError() << endl;
break;
}
//判断是否存在客户端的连接请求
if (FD_ISSET(listenSocket, &readSet)) {
SOCKADDR_IN ClientAddr;//保存客户端IP地址端口
int nLen = sizeof(ClientAddr);
//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
acceptSocket = accept(listenSocket, (sockaddr*)&ClientAddr, &nLen);
if (INVALID_SOCKET == acceptSocket) {
cout<<"accept() returned with error:" << ::WSAGetLastError() << endl;
continue;
}
else {
//将该套接字加入服务器套接字结合
FD_SET(acceptSocket, &socketSet);
}
char* pszClientIP = inet_ntoa(ClientAddr.sin_addr); //返回点分十进制的字符串在静态内存中的指针
if (NULL != pszClientIP)
{
//ntohs主要是将网络字节转为主机字节
cout << "客户端[" << pszClientIP << ":" << ntohs(ClientAddr.sin_port) <<"]请求连接成功"<< endl;
cout << "目前客户端的数量为:" << (socketSet.fd_count - 1) << endl;
//等待其他客户端连接
Sleep(1000);
}
cout << endl;
continue;
}
//遍历所有套接字
for (int i = 1; i < socketSet.fd_count; i++) {
SOCKET sAccept = socketSet.fd_array[i];//获取套接字
SOCKADDR_IN addrClient;
int nLen = sizeof(addrClient);
//获取当前连接的客户端的IP地址和端口号,即初始化addClient
getpeername(sAccept, (sockaddr*)&addrClient, &nLen);
//获取客户端地址以及它的主机字节
char* pszClientIp = inet_ntoa(addrClient.sin_addr);
unsigned short usClientPort = ntohs(addrClient.sin_port);
//该套接字可读
if (FD_ISSET(sAccept, &readSet)) {
char buf[6400] = { '\0' };
ret = recv(sAccept, buf, sizeof(buf), 0);
if (ret > 0)
{
cout << "-------------接收消息-------------" << endl;
cout << "是否显示客户端[" << pszClientIp << ":" << usClientPort << "]的消息及长度?(用1表示是,其他表示否)" << endl;
int n = 0;
cin >> n;
if (n == 1) {
cout << "消息长度为:" << ret << endl;
cout << "消息内容为:" << buf << endl << endl;
}
else {
cout << "不显示消息" << endl;
}
}else if (ret == 0) {
//对方关闭连接
cout << "客户端[" << pszClientIp << ":" << usClientPort << "]主动关闭连接" << endl;
closesocket(sAccept);
FD_CLR(sAccept, &socketSet);
cout << "目前客户端数量为:" << socketSet.fd_count - 1 << endl;
continue;
}
else {
// 客户端的socket没有被正常关闭,即没有调用closesocket
if (::WSAGetLastError() == WSAECONNRESET)
{
cout << "客户端[" << pszClientIp << ":" << usClientPort << "]被强行关闭" << endl;
}
else
{
cout << "recv data error:" << ::WSAGetLastError() << endl;
}
closesocket(sAccept);
//FD_CLR(s,set);从set集合中删除s套接字
FD_CLR(sAccept, &socketSet);
//监听socket不算客户端
printf("目前客户端的数量为: %d\n", socketSet.fd_count - 1);
continue;
}
}
//该套接字可写
if (FD_ISSET(sAccept, &writeSet)) {
t++;
if (t < socketSet.fd_count) {
char buf1[] = { "Hello,I am Server" };
cout << "-------------发送消息-------------" << endl;
cout << "是否向客户端[" << pszClientIp << ":" << usClientPort << "]发送消息(用1表示是,其他表示否)" << endl;
int n = 0;
cin >> n;
if (n == 1) {
ret = send(sAccept, buf1, sizeof(buf1), 0);
cout << "发送信息大小为:" << ret << endl;
cout << "信息内容为:" << buf1 << endl;
cout << endl;
}
}
}
}
}
//关闭监听套接字
closesocket(listenSocket);
//清理套接字库的使用
WSACleanup();
return 0;
}
Client1.cpp(其余同理)
#include<winsock2.h>//winsock的头文件
#include<iostream>
using namespace std;
//指定动态库的lib文件
#pragma comment(lib,"ws2_32.lib")
//TCP客户端
int main() {
WSADATA wsd;//WSADATA变量
int ret;//返回值
//初始化套接字动态库
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {
cout << "WSAStartup error:" << ::WSAGetLastError() << endl;
return 1;
}
//1.创建TCP Socket,流式套接字
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET) {
cout << "create socket error:" << WSAGetLastError() << endl;
return -1;
}
//2.链接服务器
sockaddr_in addr;//不建议使用sockaddr,建议使用sockaddr_in
//设置服务器地址
addr.sin_port = htons(8000);//网络字节序
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络字节序
addr.sin_family = AF_INET;//地址族
int len = sizeof(sockaddr_in);
if (connect(s, (sockaddr*)&addr, len) == SOCKET_ERROR) {
cout << "connect error:" << WSAGetLastError() << endl;
return -1;
}
//3.发送消息
char buf1[] = {"Hello! I am Client1!"};
ret = send(s, buf1, sizeof(buf1), 0);
cout << "发送信息大小为:" << ret << endl;
cout << "信息内容为:" << buf1<<endl;
cout << endl;
//4、接收消息
char buf2[6400]={ '\0' };
ret = recv(s, buf2, sizeof(buf2), 0);
cout << "接收信息大小为:" << ret << endl;
cout << "信息内容为:" << buf2 << endl;
//5.关闭套接字(判断)
cout << "是否关闭套接字?(1表示是,其他表示否)" << endl;
int n;
cin >> n;
if (n == 1) {
closesocket(s);
}
//清理winsock环境
WSACleanup();
}
3. 运行结果
- 当四个程序(一个服务端,三个客户端)同时启动时
- 而三个客户端分别向服务端发送不同的消息
- 服务端显示各客户端的消息,并一一回复它们消息
- 客户端一一退出,服务端的显示情况(分正常退出和异常退出)