网络编程--多线程服务器客户端

写在前面

此前的回声服务器/客户端都是在主线程中阻塞交互,本文将使用多线程方式实现服务器/客户端。

互斥量相关接口

使用多线程,自然避免不了线程同步问题。

因本文使用互斥量实现线程同步,因此仅介绍互斥量相关接口,其他实现线程同步的方式(如关键代码段、事件以及信号量等)可自行查阅MSDN帮助文档。

创建互斥量

使用CreateMutex创建互斥量,原型如下:

#include 
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);

成功时返回创建的互斥量对象句柄,失败返回NULL
lpMutexAttributes:传递安全相关的配置信息,使用默认安全设置时可以传递NULL
bInitialOwner:如果为TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入non-signaled状态;
如果为FALSE,则创建出的互斥量对象不属于任何线程,此时状态为signaled
lpName: 用于命名互斥量对象。传入NULL时创建无名的互斥量对象。

销毁互斥量

互斥量属于系统内核资源,使用完后需要手动释放。使用CloseHandle函数释放互斥量资源,原型如下:

BOOL CloseHandle(HANDLE hObject);

成功时返回TRUE,失败时返回FALSE
hObject 要销毁的内核对象的句柄

获取互斥量

通过WaitForSingleObject接口获取互斥量,原型如下:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

hHandle:对象的句柄。 如果等待仍在等待时关闭此句柄,则函数的行为未定义。
dwMilliseconds:超时间隔(以毫秒为单位)。 如果指定了非零值,该函数将等待对象发出信号或间隔。 如果 dwMilliseconds 为零,则如果对象未发出信号,则函数不会输入等待状态;它始终会立即返回。 如果 dwMilliseconds 为 INFINITE,则仅当发出对象信号时,该函数才会返回。

释放互斥量

使用ReleaseMutex释放互斥量,使其转变为signaled状态。

BOOL ReleaseMutex(HANDLE hMutex);

成功时返回TRUE,失败时返回FALSE
hMutex: 需要释放(解除拥有)的互斥量对象句柄

多线程服务器

多线程服务器使用一个全局的socket数组维护连接的客户端socket,在主线程中等待客户端的连接,每有一个客户端连接时就单独开启一个线程提供回声服务,使用一个全局的互斥量对象实现各提供回声服务线程的线程同步。

代码如下:

// MultiThread_Server.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include 
#include 
#include 
#pragma comment(lib, "ws2_32.lib")

using namespace std;

#define BUF_SIZE 100
#define MAX_CLNT 256

unsigned WINAPI HandleClnt(void* arg);
void SendMsg(char* arg, int len);

HANDLE hMutex;
int clntCnt = 0;
SOCKET clntSocks[MAX_CLNT];

void WriteRunLog(LPCSTR lpszLog, int len);

int _tmain(int argc, _TCHAR* argv[])
{
	if (argc != 2)
	{
		printf("argc error!\n");
		return -1;
	}

	WSADATA wsaData;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
	{
		printf("WSAStartup error!\n");
		return -1;
	}

	SOCKET srvSock = socket(PF_INET, SOCK_STREAM, 0);
	if (INVALID_SOCKET == srvSock)
	{
		printf("socket error!\n");
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_family = PF_INET;
	srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
	srvAddr.sin_port = htons(_ttoi(argv[1]));

	if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
	{
		printf("bind error!\n");
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	if (SOCKET_ERROR == listen(srvSock, 5))
	{
		printf("listen error!\n");
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN cltAddr;
	memset(&cltAddr, 0, sizeof(cltAddr));
	int nCltAddrSize = sizeof(cltAddr);

	hMutex = CreateMutex(NULL, FALSE, NULL);

	
	while (true)
	{
		//接受连接线程
		nCltAddrSize = sizeof(cltAddr);
		puts("wait for client connect...");
		SOCKET cltSock = accept(srvSock, (sockaddr*)&cltAddr, &nCltAddrSize);
		if (cltSock == INVALID_SOCKET)
		{
			printf("accept error\n");
			continue;
		}

		//等待操作互斥量数组
		WaitForSingleObject(hMutex, INFINITE);
		clntSocks[clntCnt++] = cltSock;
		ReleaseMutex(hMutex);

		//最后开启该套接字的消息处理线程
		HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, HandleClnt, (void*)&cltSock, 0, NULL);
		printf("Connected Client IP: %s \n", inet_ntoa(cltAddr.sin_addr));

	}

	CloseHandle(hMutex);
	closesocket(srvSock);
	WSACleanup();

	puts("main thread end.");
	puts("任意键继续...");
	getchar();

	return 0;
}

unsigned WINAPI HandleClnt(void* arg)
{
	SOCKET cltSock = *((SOCKET*)arg);

	int nRecvLen = 0;
	char Msg[BUF_SIZE] = {};
	char Log[2*BUF_SIZE] = {};
	while ( (nRecvLen = recv(cltSock, Msg, BUF_SIZE, 0)) != 0 )
	{
		Msg[nRecvLen] = 0;

		//sprintf(Log, "recv msg from client《%d》: %s\n", cltSock, Msg);
		//WriteRunLog(Log, strlen(Log));
		SendMsg(Msg, nRecvLen);
	}

	//若客户端断开连接,则在套接字数组中清除对应socket
	WaitForSingleObject(hMutex, INFINITE);
	//找到要清除的套接字,从该位置开始,后续元素前移覆盖删除
	//双指针实现覆盖删除
	int slow = 0;
	int fast = 0;

	for (; fast < clntCnt; fast++)
	{
		if (clntSocks[fast] == cltSock)
		{
			continue;
		}

		clntSocks[slow++] = clntSocks[fast];
	}
	clntSocks[slow] = INVALID_SOCKET;
	
	clntCnt--;
	ReleaseMutex(hMutex);
	closesocket(cltSock);

	//sprintf(Log, "Client %d Disconnected...\n", cltSock);
	//WriteRunLog(Log, strlen(Log));

	return 0;

}

void SendMsg(char* arg, int len)
{
	//回复所有客户端
	WaitForSingleObject(hMutex, INFINITE);
	char Log[2*BUF_SIZE] = {};
	for (int i = 0; i < clntCnt; i++)
	{
		//sprintf(Log, "Send to client 《%d》 msg: %s\n", clntSocks[i], len);
		//WriteRunLog(Log, strlen(Log));
		send(clntSocks[i], arg, len, 0);
	}

	ReleaseMutex(hMutex);
}

多线程客户端

此前的回声客户端均在主线程中进行读写操作,在多线程客户端中使用两个线程分别处理读写操作。

代码如下:

// MultiThread_Client.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include 
#include 
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 100
#define NAME_SIZE 20

unsigned WINAPI SendMsg(void* arg);
unsigned WINAPI RecvMsg(void* arg);

char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE] = {};


int _tmain(int argc, _TCHAR* argv[])
{
	if (argc != 4)
	{
		printf("argc error!\n");
		return -1;
	}

	WSADATA wsaData;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
	{
		printf("WSAStartup error!\n");
		return -1;
	}

	printf("server ip: %s, port: %s, client name: %s\n", argv[1], argv[2], argv[3]);
	sprintf(name, "[%s]", argv[3]);

	SOCKET cltSock = socket(PF_INET, SOCK_STREAM, 0);
	if (INVALID_SOCKET == cltSock)
	{
		puts("socket error!");
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_family = PF_INET;
	srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
	srvAddr.sin_port = htons(_ttoi(argv[2]));

	if (connect(cltSock, (sockaddr*)&srvAddr, sizeof(srvAddr)) == SOCKET_ERROR)
	{
		puts("connect error!");
		closesocket(cltSock);
		WSACleanup();
		return -1;
	}

	//开启客户端的接收和发送线程
	HANDLE hSendThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg, (void*)&cltSock, 0, NULL);
	HANDLE hRecvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void*)&cltSock, 0, NULL);

	WaitForSingleObject(hSendThread, INFINITE);
	WaitForSingleObject(hRecvThread, INFINITE);

	closesocket(cltSock);
	WSACleanup();

	puts("任意键继续...");
	getchar();

	return 0;
}

unsigned WINAPI SendMsg(void* arg)
{
	SOCKET cltSock = *((SOCKET*)arg);
	char nameMsg[NAME_SIZE + BUF_SIZE] = {};
	while (true)
	{
		//printf("Input Msg: ");
		fgets(msg, BUF_SIZE, stdin);

		if ( !strcmp(msg, "q\n") || !strcmp(msg, "Q\n") )
		{
			puts("Disconnect...");
			break;
		}

		sprintf(nameMsg, "%s %s", name, msg);
		send(cltSock, nameMsg, strlen(nameMsg), 0);
	}
	//exit(0);
	closesocket(cltSock);

	printf("client %d thread end.\n", cltSock);
	return 0;
}

unsigned WINAPI RecvMsg(void* arg)
{
	SOCKET cltSock = *((SOCKET*)arg);
	char nameMsg[NAME_SIZE + BUF_SIZE] = {};

	int nRecvLen = 0;
	while (true)
	{
		nRecvLen = recv(cltSock, nameMsg, NAME_SIZE + BUF_SIZE - 1, 0);
		if (nRecvLen == -1)
		{
			puts("server disconnected!");
			return -1;
		}
		nameMsg[nRecvLen] = 0;
		printf("nameMsg from server: %s\n", nameMsg);
	}

	return 0;
}

运行结果如下:
网络编程--多线程服务器客户端_第1张图片

总结

虽然使用互斥量实现了简单的多线程服务器/客户端,但也只是借此熟悉下线程及线程同步相关的接口,可以明显的看到效率还是比较低下的。

要想使用高效的Windows服务器客户端,可以使用IOCP完成端口实现。

你可能感兴趣的:(C++,网络编程,服务器,网络)