您的位置:首页 > 其它

socket select 模型(详细注释版本)

2018-01-24 20:43 369 查看
模型原理地址:http://blog.csdn.net/qing666888/article/details/50427685

--------------------------------------------------------------------------------

// MySelectServer.cpp : 定义控制台应用程序的入口点。
//SOCKET Select TCP服务器模型

#include "stdafx.h"
#include <WinSock2.h>
#include <string>

#pragma comment(lib, "ws2_32.lib")

#define PORT 5150

int g_iTotalConn = 0;
SOCKET g_CliSocketArr[FD_SETSIZE];

DWORD WINAPI WorkerThread(LPVOID lpParameter);

static int getPeerToString(SOCKET sock, std::string &ip, unsigned short &port);

int _tmain(int argc, _TCHAR* argv[])
{
//加载socket动态链接库
WORD wVersionRequesed; //保存要加载的winsocket版本信息
WSADATA wsaData; //这结构是用于接收Windows Socket的结构信息的

wVersionRequesed = MAKEWORD(2,2); //WINSOCK2.2版本

int err = WSAStartup(wVersionRequesed,&wsaData);
if (err!=0)
{
return -1; // 返回值为零的时候是表示成功申请WSAStartup
}

// 检查这个高字节是不是1,低字节是不是1 以确定是否我们所请求的2.2版本
// 否则的话,调用WSACleanup()清除信息,结束函数
if ( HIBYTE( wsaData.wVersion ) != 2 || LOBYTE( wsaData.wVersion ) != 2)
{
WSACleanup( );
return -1;
}

// 创建socket操作,建立流式套接字,返回套接字号sockSrv
// SOCKET socket(int af, int type, int protocol);
// 第一个参数,指定地址簇(TCP/IP只能是AF_INET,也可写成PF_INET)
// 第二个,选择套接字的类型(流式套接字),第三个,特定地址家族相关协议(0为自动)
SOCKET sockSrv = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

// 套接字sockSrv与本地地址相连
// int bind(SOCKET s, const struct sockaddr* name, int namelen);
// 第一个参数,指定需要绑定的套接字;
// 第二个参数,指定该套接字的本地地址信息,该地址结构会随所用的网络协议的不同而不同
// 第三个参数,指定该网络协议地址的长度
// PS: struct sockaddr{ u_short sa_family; char sa_data[14];};
// sa_family指定该地址家族, sa_data起到占位占用一块内存分配区的作用
// 在TCP/IP中,可使用sockaddr_in结构替换sockaddr,以方便填写地址信息
//
// struct sockaddr_in{ short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8];};
// sin_family表示地址族,对于IP地址,sin_family成员将一直是AF_INET。
// sin_port指定将要分配给套接字的端口。
// sin_addr给出套接字的主机IP地址。
// sin_zero[8]给出填充数,让sockaddr_in与sockaddr结构的长度一样。
// 将IP地址指定为INADDR_ANY,允许套接字向任何分配给本地机器的IP地址发送或接收数据。
// 如果想只让套接字使用多个IP中的一个地址,可指定实际地址,用inet_addr()函数。
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 将INADDR_ANY转换为网络字节序,调用 htonl(long型)或htons(整型)
addrSrv.sin_port = htons(PORT);

bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); // 第二参数要强制类型转换

// 将套接字设置为监听模式(连接请求), listen()通知TCP服务器准备好接收连接
// int listen(SOCKET s, int backlog);
// 第一个参数指定需要设置的套接字,第二个参数为(等待连接队列的最大长度)
listen(sockSrv, 10);

// accept(),接收连接,等待客户端连接
// SOCKET accept( SOCKET s, struct sockaddr* addr, int* addrlen);
// 第一个参数,接收一个处于监听状态下的套接字
// 第二个参数,sockaddr用于保存客户端地址的信息
// 第三个参数,用于指定这个地址的长度
// 返回的是向与这个监听状态下的套接字通信的套接字

// 客户端与用户端进行通信
// send(), 在套接字上发送数据
// int send( SOCKET s, const char* buf, int len, int flags);
// 第一个参数,需要发送信息的套接字,
// 第二个参数,包含了需要被传送的数据,
// 第三个参数是buffer的数据长度,
// 第四个参数,一些传送参数的设置

// recv(), 在套接字上接收数据
// int recv( SOCKET s, char* buf, int len, int flags);
// 第一个参数,建立连接后的套接字,
// 第二个参数,接收数据
// 第三个参数,接收数据的长度,
// 第四个参数,一些传送参数的设置

DWORD dwThreadId;
// 创建一个工作者线程
CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);

SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);

while (g_iTotalConn < FD_SETSIZE)
{
// 接受一个客户端连接
SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);

printf("客户端已经连接:%s:%d\n", inet_ntoa(addrClient.sin_addr), ntohs(addrClient.sin_port));

// 把已经连接的客户端添加到全局数组中
g_CliSocketArr[g_iTotalConn] = sockConn;

g_iTotalConn++;
}

return 0;
}

#define MSGSIZE 1024 //接受的最大缓冲区大小

DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int i;
fd_set fdread;
int ret;
struct timeval tv = {1, 0};
char szMessage[MSGSIZE];

while (TRUE)
{
FD_ZERO(&fdread);
for (i = 0; i < g_iTotalConn; i++)
{
FD_SET(g_CliSocketArr[i], &fdread);
}

//int select
// (
// int nfds, //Winsock中此参数无意义
// fd_set* readfds, //进行可读检测的Socket
// fd_set* writefds, //进行可写检测的Socket
// fd_set* exceptfds, //进行异常检测的Socket
// const struct timeval* timeout //非阻塞模式中设置最大等待时间
// )

/*struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态:
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
*/
// 只关心读取事件
ret = select(0, &fdread, NULL, NULL, &tv);

//因为如果读集中没有任何套接字,select函数会立刻返回-1,这将导致工作者线程成为一个毫无停顿的死循环,CPU的占用率马上达到100%。
if (ret == -1)
{
Sleep(1);
}

//0:等待超时,没有可读写或错误的文件
if (ret == 0)
{
// Time expired
continue;
}

for (i = 0; i < g_iTotalConn; i++)
{
//判断描述符fd是否在给定的描述符集fdset中,通常配合select函数使用,由于select函数成功返回时会将未准备好的描述符位清零。
//通常我们使用FD_ISSET是为了检查在select函数返回后,某个描述符是否准备好,以便进行接下来的处理操作。
if (FD_ISSET(g_CliSocketArr[i], &fdread))
{
// 一个读取事件发生在 g_CliSocketArr[i]
ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0);

if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
// 客户端关闭 把后面的SOCKET往前移动一个
printf("Client socket %d closed.\n", g_CliSocketArr[i]);
closesocket(g_CliSocketArr[i]);

//把当前关闭的SOCKET后面的SOCKET数组前移动一个位置
if (i<=g_iTotalConn -1)
{
for (int j =0;j<g_iTotalConn - i -1;j++)
{
g_CliSocketArr[i+j] = g_CliSocketArr[i+j+1];
}
g_iTotalConn--;
}
}
else
{
//获取SOCKET的IP和端口
std::string Ip;
unsigned short port;
getPeerToString(g_CliSocketArr[i],Ip,port);

// 从客户端接受到的信息
szMessage[ret] = '\0';
printf("客户端:%s 端口:%d 接收到客户端数据:%s\n", Ip.c_str(),port,szMessage);

//把接收到信息再发送给客户端
send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);
}
}
}
}

return 0;
}

//获取SOCKET的IP和端口
static int getPeerToString(SOCKET sock, std::string &ip, unsigned short &port)
{
struct sockaddr_storage sa;
int salen = sizeof(sa);
if (::getpeername(sock, (struct sockaddr*)&sa, &salen) == -1)
{
ip = "?";
port = 0;
return -1;
}

if (sa.ss_family == AF_INET)
{
struct sockaddr_in *s = (struct sockaddr_in*)&sa;
ip = ::inet_ntoa(s->sin_addr);
port = ::ntohs(s->sin_port);
return 0;
}
return -1;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: