写在前面
此前的回声服务器/客户端都是在主线程中阻塞交互,本文将使用多线程方式实现服务器/客户端。
互斥量相关接口
使用多线程,自然避免不了线程同步问题。
因本文使用互斥量实现线程同步,因此仅介绍互斥量相关接口,其他实现线程同步的方式(如关键代码段、事件以及信号量等)可自行查阅MSDN帮助文档。
创建互斥量
使用CreateMutex创建互斥量,原型如下:
#include <windows.h>
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 <process.h>
#include <WinSock2.h>
#include <string>
#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 <process.h>
#include <WinSock2.h>
#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;
}
运行结果如下:
总结
虽然使用互斥量实现了简单的多线程服务器/客户端,但也只是借此熟悉下线程及线程同步相关的接口,可以明显的看到效率还是比较低下的。
要想使用高效的Windows服务器客户端,可以使用IOCP完成端口实现。