Loading

Socket 编程

 

基本知识

socket 编程有三种,流式套接字 SOCK_STREAM ,数据报套接字 SOCK_DGRAM ,原始套接字 SOCK_RAW ,前两者较常用。基于 TCP 的 socket 编程是流式套接字。

 

Socket(套接字):一种应用程序接口 API

TCP:建立在 IP 之上,通过握手建立连接,对每个 IP 包编号,确保对方按顺序收到,如果包丢失,就自动重发

UDP:建立在 IP 之上,但面向无连接的通信协议,不能保证数据包的顺利到达,是不可靠传输,但传输效率比 TCP 高

 

WinSocket 包含 Socket 和 Socket2 ,一般使用 Socket2 ,在使用时需要包含如下头文件和 LIB 文件

#include <WinSock.h> //一般导入下面那个
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

使用 g++ 编译 Socket 程序时,要在“工具-编译选项”中加入 -lwsock32 参数,与窗口程序同时使用按如下格式:

-mwindows -lwsock32

 

既然涉及到 IP 地址,我们需要掌握相关的表现形式。在命令行中输入 ipconfig 查看本机 ip 地址

ipconfig

 

IP 地址常用点分法表示,如 192.168.0.169 ,即 4 个 0-255 的整数表示,计算机中不使用这种方法保存 IP ,因为浪费存储空间且不利于计算,因此采用无符号长整型数储存 IP 地址。

如果我们需要保存和传递 IP 地址,就要按照特定的形式进行转换。在网络传输中 IP 地址保存为 32 位二进制,在低位存储中保存数据的高位字节。这种存储顺序格式被称为网络字节顺序,按照 32 位二进制数为一组进行传输,数据传输顺序由高到低。在 VC 中使用 in_addr 类型来保存 IP 地址,另外还有转换函数

// IP 转地址字符串
char FAR * inet_ntoa(
    struct in_addr in
);

// 地址字符串转 IP
unsigned long inet_addr(
    const char FAR * cp
);

例如

in_addr addr;
addr.s_addr = inet_addr("198.168.0.12");

 

不同主机对IP地址的存储格式不同,需要通过以下函数实现主机和网络字节顺序的互转(host to network u_long):

//  将主机字节顺序格式 IP 转为 TCP/IP 网络字节顺序
u_long htonl(
    u_long hostlong
);

// 主机转网络 u_short 型
u_short htons(
    u_short hostshort
);

// 网络转主机 long
u_long ntohl(
    u_long netlong
);

// 网络转主机 short
u_short ntohs(
    u_short netshort
);

 

结构变量

首先创建版本号

WORD wVersion = MAKEWORD(2, 2);  // Socket2 版本

 

还需要初始化 WinSocket

Int WSAStartup(
    WORD wVersionRequested,  //版本号
    LPWSADATA lpWSAData
); 

其中 WSAData 结构用于存放返回的 Socket 数据,它有结构

typedef struct WSAData {
    WORD wVersion; 							// 期望用户使用的规范版本
    WORD wHighVersion; 						// 可支持的最高版本
    char szDescription[WSADESCRIPTION_LEN+1]; // 描述字符串
    char szSystemStatus[WSASYS_STATUS_LEN+1]; // 状态字符串
    unsigned short iMaxSockets; 			 // 套接字最大编号
    unsigned short iMaxUdpDg; 				 // 忽略
    char FAR * lpVendorInfo; 				 // 废弃 
} WSADATA, FAR * LPWSADATA;

如果返回 0 表示初始化成功

 

创建 Socket 变量的函数

SOCKET socket(
    int af,			// 套接字地址
    int type,		// 套接字类型(此处为流式套接字 TCP )
    int protocol	// 使用协议(TCP)
);

于是我们定义服务端的 Socket 变量

SOCKET hServer;
hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

if (hServer==INVALID_SOCKET) 
{
	// 初始化失败
}

其中 PF_INET 和 AF_INET 是等价的,为了安全起见,需要判断初始化是否成功再进行操作

 

建立连接还需要提供接收端口

sockaddr_in addrServer; 						// 定义端口结构
addrServer.sin_family = AF_INET;				 // 设置端口地址
addrServer.sin_port = htons(8888);				 // 网络字节
addrServer.sin_addr.s_addr = htonl(INADDR_ANY);	  // 主机 IP

特别的,'127.0.0.1' 表示本机 IP 地址, 8888 为端口号, INADDR_ANY 表示不指定 ip

注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定

 

使用 bind 函数绑定端口到本地地址

int bind(
    SOCKET s,							// 套接字
    const struct sockaddr FAR * name,	  // 端口名称
    int namelen							// 名称长度
);

例如我们建立服务端绑定

int nRet = bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer));

if (nRet==SOCKET_ERROR) 
{
	// 绑定失败,释放套接字
	closesocket(hServer);
	WSACleanup();
}

 

通过 listen 函数进行数据监听

int listen(
    SOCKET s,		// 套接字
    int backlog		// 最大连接数
);

例如设置最大监听数为 1

nRet = listen(hServer, 1); //最大连接数设为1

if (nRet==SOCKET_ERROR) 
{
    // 绑定失败,释放套接字
    closesocket(hServer);
    WSACleanup();
}

 

服务端通过 accept 获取客户端连接请求

SOCKET accept(
    SOCKET s,					// 套接字
    struct sockaddr FAR * addr,	  // 端口地址
    int FAR * addrlen			 // 地址长度
);

我们需要先为客户端也定义套接字,然后进行连接

SOCKET hClient; 										// 用户套接字
sockaddr_in addrClient; 								 // 用户结构
int nLen = sizeof(addrClient);							  // 地址长度
hClient = accept(hServer, (sockaddr *)&addrServer, &nLen);  // 接收客户端信息

if (hClient==INVALID_SOCKET) 
{
    // 接收失败
    closesocket(hServer);
    WSACleanup();
}

而对于客户端,需要通过 connect 连接服务端

int connect(
    SOCKET s,							// 套接字
    const struct sockaddr FAR * name,	  // 端口名
    int namelen							// 长度
);

 

通过 send 函数向对方发送数据

int send(
    SOCKET s,				// 套接字
    const char FAR * buf,	 // 发送数据
    int len,				// 数据长度
    int flags				// 风格
);

 

最后就是循环接收客户端数据,需要重复调用 recv 函数

int recv(
    SOCKET s,			// 套接字
    char FAR * buf,		 // 接收缓冲区
    int len,			// 缓冲区长度
    int flags			// 风格
);

我们给出一个示例:

char szBuf[255];	// 定义缓存区

while (TRUE) 
{
    memset(szBuf, 0, sizeof(char)*255);					// 清空缓存区
    nRet = recv(hClient, szBuf, sizeof(char)*255, 0);	 // 接收数据

    if (nRet==SOCKET_ERROR) 
    {
        // 接收失败,关闭连接
        closesocket(hClient);
        closesocket(hServer);
        WSACleanup();
    }
}

 

两端连接

对于服务端,我们分成几个块来实现这几个流程,注意它们中有些部分是可以依次执行的,但是建议将每一部分拆成一个函数来使用。需要的前置声明:

#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#include <cstdlib>
#include <iomanip>
#include <iostream>

#define BUFSIZE 255

客户端始终是一样的,简短且不需要分块;复杂的操作和阻塞式区别出现在服务端,因此只需关注服务端的分块流程

 

服务端

建立socket,申明自身的 port 和 IP ,并绑定到 socket ,使用 listen 监听,然后不断用 accept 去查看是否有连接。如果有,捕获 socket ,并通过 recv 获取消息的内容,通信完成后调用 closeSocket 关闭这个对应 accept 到的 socket ,如果不需要等待任何客户端连接,那么用 closeSocket 直接关闭自身的 socket

初始化过程:

// 创建版本号
WORD wVersion = MAKEWORD(2, 2);
// 创建 WSADATA 结构
WSADATA wsaData;
// 初始化 WinSocket
if (WSAStartup(wVersion, &wsaData) == 0)
{
	std::cout << "SOCKET初始化" << std::endl;
}
else
{
	std::cout << "初始化失败" << std::endl;
}

 

定义端口结构,绑定本地地址,这一部分作为函数要返回服务器接口

// 服务端接口
SOCKET hServer;
hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hServer == INVALID_SOCKET)
{
	std::cout << "初始化失败" << std::endl;
}
else
{
	std::cout << "套接字初始化成功!" << std::endl;
}

// 接收端口
sockaddr_in addrServer; // 定义端口结构
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8888);
addrServer.sin_addr.s_addr = htonl(INADDR_ANY);
// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定

// 绑定本地地址( nRet 获取返回值)
int nRet = bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer));
if (nRet == SOCKET_ERROR)
{
	// 绑定失败,释放套接字
	std::cout << "绑定失败!" << std::endl;
	closesocket(hServer);
	WSACleanup();
}
else
{
	std::cout << "绑定套接字成功!" << std::endl;
}

// 数据监听
nRet = listen(hServer, 1); // 最大连接数设为 1
if (nRet == SOCKET_ERROR)
{
	// 绑定失败,释放套接字
	std::cout << "监听失败!" << std::endl;
	closesocket(hServer);
	WSACleanup();
}
else
{
	std::cout << "开始监听..." << std::endl;
}

 

获取客户端请求

// 获取客户端请求
SOCKET hClient;			// 用户套接字
sockaddr_in addrClient; // 用户结构
int nLen = sizeof(addrClient);
hClient = accept(hServer, (sockaddr *)&addrServer, &nLen); // 接收客户端信息
if (hClient == INVALID_SOCKET)
{
	// 接收失败
	std::cout << "接收失败,连接中断!" << std::endl;
	closesocket(hServer);
	closesocket(hClient);
	WSACleanup();
}
else
{
	std::cout << "连接客户端成功!" << std::endl;
}

std::cout << "连接完成,开始数据传输..." << std::endl;

 

循环接收客户端数据

// 循环接收客户端数据
char recvBuf[BUFSIZE];
char sendBuf[BUFSIZE];
while (TRUE)
{
	nRet = recv(hClient, recvBuf, sizeof(char) * BUFSIZE, 0);
	if (nRet == SOCKET_ERROR)
	{
		// 接收失败
		std::cout << "接收失败,连接中断!" << std::endl;
		break;
	}
	std::cout << "客户端消息:" << recvBuf << std::endl;
	if (strcmp(recvBuf, "close") == 0)
	{
		nRet = send(hClient, "close", strlen("close"), 0);
		std::cout << "连接中断" << std::endl;
		break;
	}
	else
	{
		std::cout << "回复消息:";
		std::cin >> sendBuf;
		nRet = send(hClient, sendBuf, strlen(sendBuf), 0);
		if (nRet == SOCKET_ERROR)
		{
			std::cout << "传输出错,连接中断" << std::endl;
			break;
		}
	}
	// 清空缓冲区
	memset(recvBuf, 0, sizeof(char) * BUFSIZE);
	memset(sendBuf, 0, sizeof(char) * BUFSIZE);
}

 

释放套接字

// 释放 Socket
closesocket(hClient);
closesocket(hServer);
WSACleanup();

 

客户端

建立 socket ,通过端口号和地址确定目标服务器,使用 Connect 连接到服务器, send 发送消息,等待处理,通信完成后调用 closeSocket 关闭 socket

int main()
{
	// 创建版本号
	WORD wVersion = MAKEWORD(2, 2);
	// 创建 WSADATA 结构
	WSADATA wsaData;
	// 初始化 WinSocket
	if (WSAStartup(wVersion, &wsaData) == 0)
	{
		std::cout << "SOCKET初始化" << std::endl;
	}
	else
	{
		std::cout << "初始化失败" << std::endl;
	}

	// 服务端接口
	SOCKET hServer;
	hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (hServer == INVALID_SOCKET)
	{
		std::cout << "初始化失败" << std::endl;
	}
	else
	{
		std::cout << "套接字初始化成功!" << std::endl;
	}

	// 接收端口
	sockaddr_in addrServer; // 定义端口结构
	addrServer.sin_family = AF_INET;
	addrServer.sin_port = htons(8888);
	addrServer.sin_addr.s_addr = inet_addr("127.0.0.1"); // 绑定到目标 IP 地址
	// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
	// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定

	// 连接服务器( nRet 获取返回值)
	int nRet = connect(hServer, (sockaddr *)&addrServer, sizeof(addrServer));
	if (nRet == SOCKET_ERROR)
	{
		// 连接失败,释放套接字
		std::cout << "连接失败!" << std::endl;
		closesocket(hServer);
		WSACleanup();
	}
	else
	{
		std::cout << "连接服务器成功!" << std::endl;
	}

	// 向服务器端发送请求
	char recvBuf[BUFSIZE];
	char sendBuf[BUFSIZE];
	while (TRUE)
	{
		std::cout << "发送消息:";
		std::cin >> sendBuf;
		nRet = send(hServer, sendBuf, strlen(sendBuf), 0);
		if (nRet == SOCKET_ERROR)
		{
			std::cout << "传输出错,连接中断" << std::endl;
			break;
		}
		// 接收消息
		nRet = recv(hServer, recvBuf, sizeof(char) * BUFSIZE, 0);
		if (nRet == SOCKET_ERROR)
		{
			// 接收失败
			std::cout << "接收失败,连接中断!" << std::endl;
			break;
		}
		if (strcmp(recvBuf, "close") == 0)
		{
			nRet = send(hServer, "close", strlen("close"), 0);
			std::cout << "连接中断" << std::endl;
			break;
		}
		else
		{
			std::cout << "服务端消息:" << recvBuf << std::endl;
		}
		// 清空缓冲区
		memset(recvBuf, 0, sizeof(char) * BUFSIZE);
		memset(sendBuf, 0, sizeof(char) * BUFSIZE);
	}

	// 释放 Socket
	closesocket(hServer);
	WSACleanup();

	std::system("pause");

	return 0;
}

 

非阻塞式

在创建一个套接字后,默认是阻塞式的, WinSocket 中的 I/O 函数: Send 和 Recv 操作必须等待完成相应的 I/O 操作后才能继续执行,每次只服务一个连接,只有当当前客户端连接断开后才会接入下一个客户端,因此阻塞式并发连接模式需要通过多线程同时服务多个连接,关于多线程的操作在另一个文档中记录。

相对的,无论操作是否完成,非阻塞式函数立即返回,它采用的是轮流监听的模式。我们可以调用

int ioctlsocket(
    SOCKET s,			// 套接字
    long cmd,			// 命令
    u_long FAR * argp	 // 标记套接字模式
);

函数改变套接字模式,例如将套接字设为非阻塞式

u_long nNoBlock = 1; 
ioctlsocket(s, FIONBIO, (u_long FAR*)&nNoBlock);

除此之外的流程都较为类似

 

初始化过程:

// 创建版本号
WORD wVersion = MAKEWORD(2, 2);
// 创建 WSADATA 结构
WSADATA wsaData;
// 初始化 WinSocket
if (WSAStartup(wVersion, &wsaData) == 0)
{
	std::cout << "SOCKET初始化" << std::endl;
}
else
{
	std::cout << "初始化失败" << std::endl;
}

 

定义端口结构,绑定本地地址,这一部分作为函数要返回服务器接口

// 服务端接口
SOCKET hServer;
hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hServer == INVALID_SOCKET)
{
	std::cout << "初始化失败" << std::endl;
}
else
{
	std::cout << "套接字初始化成功!" << std::endl;
}

// 接收端口
sockaddr_in addrServer; // 定义端口结构
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8888);
addrServer.sin_addr.s_addr = htonl(INADDR_ANY);
// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定

// 绑定本地地址( nRet 获取返回值)
int nRet = bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer));
if (nRet == SOCKET_ERROR)
{
	// 绑定失败,释放套接字
	std::cout << "绑定失败!" << std::endl;
	closesocket(hServer);
	WSACleanup();
}
else
{
	std::cout << "绑定套接字成功!" << std::endl;
}

// 数据监听
nRet = listen(hServer, 1); // 最大连接数设为 1
if (nRet == SOCKET_ERROR)
{
	// 绑定失败,释放套接字
	std::cout << "监听失败!" << std::endl;
	closesocket(hServer);
	WSACleanup();
}
else
{
	std::cout << "开始监听..." << std::endl;
}

 

设置非阻塞模式,获取客户端请求,这里的代码表示持续等待直到连接到客户端为止

// 设置非阻塞模式
int iMode = 1; // 为 1 表示非阻塞, 0 表示阻塞

// 第二个类型为 FIONBIO 可用
nRet = ioctlsocket(hSocket, FIONBIO, (u_long FAR *)&iMode); //设为非阻塞式

// 创建客户端套接字
SOCKET hClient;			//用户套接字
sockaddr_in addrClient; //用户端口
int nLen = sizeof(addrClient);

while (true)
{
	// 由于是非阻塞式,该函数会立即返回
	hClient = accept(hServer, (sockaddr *)&addrServer, &nLen); // 连接客户端
	// 因此要判断返回套接字是否是由于未接收到客户端而无效
	if (hClient == INVALID_SOCKET)
	{
		int nErrorCode = WSAGetLastError();
		// 如果是未接受到客户端,则暂停然后继续循环
		if (nErrorCode == WSAEWOULDBLOCK)
		{
			// 连接请求不能立即执行,先休眠然后继续
			Sleep(100);
			continue;
		}
		else
		{
			// 否则出现错误
			closesocket(hSocket);
			WSACleanup();
			break;
		}
	}
}

 

收发消息

// 收发消息
while (true)
{
	// 初始化缓存区
	memset(Buf, 0, BUF_SIZE);
	nRet = recv(hClient, Buf, BUF_SIZE, 0);

	// 同样判断立即返回的信息是否是尚未接收数据造成的错误
	if (nRet == SOCKET_ERROR)
	{
		int nErrorCode = WSAGetLastError();
		if (nErrorCode == WSAEWOULDBLOCK)
		{
			// 缓存区暂无数据,先休眠然后继续
			Sleep(100);
			continue;
		}
		else if (nErrorCode == WSAETIMEDOUT || nErrorCode == WSAENETDOWN) // 超时或网络错误
		{
			closesocket(hSocket);
			closesocket(hClient);
			WSACleanup();
			break;
		}
	}
	// 返回消息时同理省略 ...
}

 

释放套接字

// 释放 Socket
closesocket(hServer);
closesocket(hServer);
WSACleanup();

 

多机连接

通常我们并不只建立两个主机之间的连接,服务端需要同时处理多个客户端传来的数据,因此我们在下面给出一个服务端创建的范例。客户端只需要建立与服务端的连接,因此不需要修改。

首先列出所有需要的函数的声明:

bool init();							   // 初始化 WSASocket
void debugLog(std::string debug);		   	 // 调试错误输出
void debugTip(std::string tip);			   	 // 运行提示
SOCKET bindListen(int linkNum);			   	 // 建立服务端端口,绑定本地地址,开启监听,返回服务器端套接字
SOCKET buildConnection(SOCKET hServer);	   	  // 连接一个客户端,返回客户端套接字
bool clientConnect(SOCKET hClient);		   	 // 处理一个客户端连接进行收发数据
DWORD WINAPI clientThread(LPVOID lpParam); 	  // 客户端线程
bool closeConnect(SOCKET hClient);		   	 // 关闭一个连接
void myTCP();							   // 服务器主体

 

主函数是非常简单的,我们把复杂操作全部放到了函数中处理

int main()
{
    init();		  		// 初始化
    myTCP();	  		// 数据处理
    WSACleanup(); 		// 释放 WinSocket
    std::system("pause");

    return 0;
}

 

两个调试用的函数定义

// 调试输出错误
void debugLog(std::string debug)
{
	std::cout << debug << ",错误:" << WSAGetLastError() << std::endl;
}

// 运行提示
void debugTip(std::string tip)
{
	std::cout << "系统消息:" << tip << std::endl;
}

 

初始化过程

bool init()
{
	// 创建版本号
	WORD wVersion = MAKEWORD(2, 2);
	// 创建 WSADATA 结构
	WSADATA wsaData;
	// 初始化 WinSocket
	if (WSAStartup(wVersion, &wsaData) == 0)
	{
		debugTip("Socket初始化成功");
		return true;
	}
	else
	{
		debugLog("Socket初始化失败");
		return false;
	}
}

 

完成初始化后,可以看到使用 myTCP 函数,这个函数集成了多个步骤,监听过程和接收处理都在其中

void myTCP()
{
	SOCKET hSocket = bindListen(1);
	if (hSocket == INVALID_SOCKET)
	{
		debugLog("服务器初始化失败");
		return;
	}

	while (true)
	{
		// 返回客户端套接字
		SOCKET hClient = buildConnection(hSocket);
		if (hClient == INVALID_SOCKET)
		{
			debugLog("客户端连接失败");
			break;
		}
		// 当接收到客户端连接请求就为他开一个线程
		DWORD dwThreadId;
		HANDLE hThread = CreateThread(0, 0, clientThread, (LPVOID)hClient, 0, &dwThreadId);
		if (hThread)
		{
			// 线程退出后释放句柄,防止句柄占用导致程序崩溃
			CloseHandle(hThread);
		}
		//		// 处理客户端进程
		//		if (clientConnect(hClient)==false) {
		//			debugLog("建立进程失败");
		//		}
		//		// 关闭一个客户端连接
		//		if (closeConnect(hClient)==false) {
		//			debugLog("关闭客户端失败");
		//		}
	}
	if (closesocket(hSocket) == SOCKET_ERROR)
	{
		debugLog("关闭socket失败");
	}
	else
	{
		debugTip("socket关闭");
	}
}

之所以采用这种形式而非前面那种流程,是因为要建立多个连接,就必须循环建立客户端连接,每次都检测是否还有可连接的客户端,因此需要频繁调用 buildConnection 函数;注意其中注释掉的部分,那原本是用于处理客户端数据和关闭客户端的部分,但是为了能够同时处理多个客户端的进程,我们采用了多线程函数 clientThread ,它在前面通过 CreateThread 构建,因此取代了之前的处理方法。

 

当然,在最开始,还需要调用 bindListen 函数,它用于建立服务端端口,绑定本地地址,开启监听,返回服务器端套接字

SOCKET bindListen(int linkNum)
{
	// 服务端接口
	SOCKET hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (hServer == INVALID_SOCKET)
	{
		debugLog("服务器端口初始化失败");
		return INVALID_SOCKET;
	}
	else
	{
		debugTip("服务器端口初始化成功...");
	}

	// 接收端口
	sockaddr_in addrServer; // 定义端口结构
	addrServer.sin_family = AF_INET;
	addrServer.sin_port = htons(8888);
	addrServer.sin_addr.s_addr = htonl(INADDR_ANY);
	// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
	// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定

	// 绑定本地地址
	if (bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer)) == SOCKET_ERROR)
	{
		// 绑定失败,释放套接字
		debugLog("绑定失败");
		closesocket(hServer);
		WSACleanup();
		return INVALID_SOCKET;
	}
	else
	{
		debugTip("绑定成功...");
	}

	// 数据监听,最大连接数设为 linkNum
	if (listen(hServer, linkNum) == SOCKET_ERROR)
	{
		// 绑定失败,释放套接字
		debugLog("监听失败");
		closesocket(hServer);
		WSACleanup();
		return INVALID_SOCKET;
	}
	else
	{
		debugTip("开始监听...");
	}

	return hServer;
}

 

需要循环调用的连接客户端,返回客户端套接字的函数

SOCKET buildConnection(SOCKET hServer)
{
	// 获取客户端请求
	sockaddr_in addrClient; // 客户端结构
	int nLen = sizeof(addrClient);

	SOCKET hClient = accept(hServer, (sockaddr *)&addrClient, &nLen); // 接收客户端连接
	if (hClient == INVALID_SOCKET)
	{
		// 接收失败
		debugLog("接收失败,连接中断!");
		closesocket(hServer);
		closesocket(hClient);
		WSACleanup();
		return INVALID_SOCKET;
	}
	else
	{
		debugTip("连接客户端成功!");
	}

	debugTip("连接完成,开始数据传输...");
	return hClient;
}

 

由于存在多个连接,我们使用多线程处理

// 线程处理逻辑
DWORD WINAPI clientThread(LPVOID lpParam)
{
	SOCKET hClient = (SOCKET)lpParam;
	// 处理客户端进程
	if (clientConnect(hClient) == false)
	{
		debugTip("进程退出");
	}
	// 关闭一个客户端连接
	if (closeConnect(hClient) == false)
	{
		debugTip("客户端关闭");
	}
	return 0;
}

 

在该函数中调用处理和关闭客户端的函数:在循环中处理一个客户端连接,进行收发数据

bool clientConnect(SOCKET hClient)
{
	// 循环接收客户端数据
	char recvBuf[BUFSIZE];
	char sendBuf[BUFSIZE];
	while (TRUE)
	{
		// SOCKET_ERROR == 0
		if (recv(hClient, recvBuf, BUFSIZE, 0) == SOCKET_ERROR)
		{
			// 接收失败
			debugLog("接收失败,连接中断");
			return false;
		}
		std::cout << "客户端消息:" << recvBuf << std::endl;
		if (strcmp(recvBuf, "close") == 0)
		{
			send(hClient, "close", strlen("close"), 0);
			debugTip("连接中断");
			return false;
		}
		else
		{
			std::cout << "回复消息:";
			std::cin >> sendBuf;
			if (send(hClient, sendBuf, strlen(sendBuf), 0) == SOCKET_ERROR)
			{
				debugTip("传输出错,连接中断");
				return false;
			}
		}
		// 清空缓冲区
		memset(recvBuf, 0, BUFSIZE);
		memset(sendBuf, 0, BUFSIZE);
	}

	return true;
}

 

关闭客户端连接的函数

// 关闭一个连接
bool closeConnect(SOCKET hClient)
{
	// 首先发送一个 TCP FIN 分段,向对方表明已经完成数据发送
	if (shutdown(hClient, SD_SEND) == SOCKET_ERROR)
	{
		debugLog("连接关闭失败");
		return false;
	}
	char Buf[BUFSIZE];
	int nRetByte;

	do
	{
		nRetByte = recv(hClient, Buf, BUFSIZE, 0);
		if (nRetByte == SOCKET_ERROR)
		{
			debugLog("接收失败");
			return false;
		}
		else if (nRetByte > 0)
		{
			debugTip("最后的接收数据");
		}
	} while (nRetByte != 0);
	if (closesocket(hClient) == SOCKET_ERROR)
	{
		debugLog("socket关闭失败");
		return false;
	}

	return true;
}
posted @ 2022-03-13 22:15  Bluemultipl  阅读(59)  评论(0编辑  收藏  举报