来源:微信公众号「编程学习基地」
C语言聊天室
基于 tcp 实现群聊功能,本项目设计是在windows环境下基于套接字(Socket)和多线程编程进行开发的简易聊天室,实现了群聊功能,在VC6.0和VS2019运行测试无误。
运行效果
分析设计
Windows下基于windows网络接口Winsock的通信步骤为WSAStartup 进行初始化--> socket 创建套接字--> bind 绑定--> listen 监听--> connect 连接--> accept 接收请求--> send/recv 发送或接收数据--> closesocket 关闭 socket--> WSACleanup 最终关闭。
了解完了一个 socket 的基本步骤后我们了解一下多线程以及线程的同步。
多线程
线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件、信号标识及动态分配的内存等。一个进程内的所有线程使用同一个地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行以及什么时候执行线程。
简而言之多线程是为了提高系统的运行效率。
Win32 API下的多线程编程 也就是两个函数的应用CreateThread
以及WaitForSingleObject
,具体案例这里不多做介绍。
线程的同步
每个线程都可以访问进程中的公共变量,资源,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。数据之间的相互制约包括
1、直接制约关系,即一个线程的处理结果,为另一个线程的输入,因此线程之间直接制约着,这种关系可以称之为同步关系
2、间接制约关系,即两个线程需要访问同一资源,该资源在同一时刻只能被一个线程访问,这种关系称之为线程间对资源的互斥访问,某种意义上说互斥是一种制约关系更小的同步
windows线程间的同步方式有四种:临界区、互斥量、信号量、事件。
本项目是基于事件内核对象实现的线程同步,事件内核对象是一种抽象的对象,有受信和未授信两种状态,通过等待WaitForSingleObject
实现线程同步。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性
BOOL bManualReset, //是否手动重置事件对象为未受信对象
BOOL bInitialState, //指定事件对象创建时的初始状态
LPCSTR lpName //事件对象的名称
);
设置内核对象状态
BOOL SetEvent(
HANDLE hEvent /*设置事件内核对象受信*/
);
BOOL ResetEvent(
HANDLE hEvent /*设置事件内核对象未受信*/
);
堵塞等待事件内核对象直到事件内核对象的状态为受信。
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
具体使用阅读全文在我的个人网站里看,篇幅太多。
服务端设计
在创建套接字绑定监听之后会有一个等待连接的过程,在接收到新连接之后,需要创建一个线程来处理新连接,当有多个新连接时可通过创建多个线程来处理新连接,
定义最大连接数量以及最大套接字和最大线程
#define MAX_CLNT 256
int clnt_cnt = 0; //统计套接字
int clnt_socks[MAX_CLNT]; //管理套接字
HANDLE hThread[MAX_CLNT]; //管理线程
当有新连接来临的时候创建线程处理新连接,并将新连接添加到套接字数组里面管理
hThread[clnt_cnt] = CreateThread(
NULL, // 默认安全属性
NULL, // 默认堆栈大小
ThreadProc, // 线程入口地址(执行线程的函数)
(void*)&clnt_sock, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
clnt_socks[clnt_cnt++] = clnt_sock;
线程的处理函数ThreadProc不做讲解,大致就是数据的收以及群发。
主要讲解线程同步,当有多个新连接来临的时候,可能会造成多个线程同时访问同一个数据(例如clnt_cnt)。这个时候就需要线程的同步来避免破坏数据的完整性。
首先是创建一个内核事件
HANDLE g_hEvent; /*事件内核对象*/
// 创建一个自动重置的(auto-reset events),受信的(signaled)事件内核对象
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
然后再需要访问公共变量(例如clnt_cnt
)之前进行加锁(设置等待),访问完成之后解锁(设置受信)
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE);
hThread[clnt_cnt] = CreateThread(NULL,NULL,ThreadProc,(void*)&clnt_sock,0,&dwThreadId);
clnt_socks[clnt_cnt++] = clnt_sock;
SetEvent(g_hEvent); /*设置受信*/
通过套接字数组来进行数据的转发实现群聊功能,此时也用到了线程同步
void send_msg(char* msg, int len)
{
int i;
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE);
for (i = 0; i < clnt_cnt; i++)
send(clnt_socks[i], msg, len, 0);
SetEvent(g_hEvent); /*设置受信*/
}
遇到的问题
等待线程返回的过程中最先用的是WaitForSingleObject
,很遗憾这是个阻塞函数,直到线程执行完成返回之后才会继续往下执行,所以后面通过WaitForMultipleObjects
这个windowsAPI调用对hThread线程数组进行线程等待释放。
整个过程不算太难,主要是仅仅实现了群聊功能,所以只需要了解windows下的网络编程以及多线程编程和线程的同步方法就可以实现这个样一个功能。
源代码:
server.c
#include <winsock2.h> // 为了使用Winsock API函数
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define MAX_CLNT 256
#define BUF_SIZE 100
// 告诉连接器与WS2_32库连接
#pragma comment(lib,"WS2_32.lib")
void error_handling(const char* msg); /*错误处理函数*/
DWORD WINAPI ThreadProc(LPVOID lpParam); /*线程执行函数*/
void send_msg(char* msg, int len); /*消息发送函数*/
HANDLE g_hEvent; /*事件内核对象*/
int clnt_cnt = 0; //统计套接字
int clnt_socks[MAX_CLNT]; //管理套接字
HANDLE hThread[MAX_CLNT]; //管理线程
int main()
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
WSAStartup(sockVersion, &wsaData); //请求了一个2.2版本的socket
// 创建一个自动重置的(auto-reset events),受信的(signaled)事件内核对象
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
// 创建套节字
SOCKET serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serv_sock == INVALID_SOCKET)
error_handling("Failed socket()");
// 填充sockaddr_in结构
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8888); //8888端口
sin.sin_addr.S_un.S_addr = INADDR_ANY; //本地地址
//sin.sin_addr.S_un.S_addr = inet_addr("169.254.211.52");
// 绑定这个套节字到一个本地地址
if (bind(serv_sock, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
error_handling("Failed bind()");
// 进入监听模式
if (listen(serv_sock, 256) == SOCKET_ERROR) //最大连接数为2
error_handling("Failed listen()");
printf("Start listen:\n");
// 循环接受客户的连接请求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
DWORD dwThreadId; /*线程ID*/
SOCKET clnt_sock;
//char szText[] = "hello!\n";
while (TRUE)
{
printf("等待新连接\n");
// 接受一个新连接
clnt_sock = accept(serv_sock, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (clnt_sock == INVALID_SOCKET)
{
printf("Failed accept()");
continue;
}
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE);
hThread[clnt_cnt] = CreateThread(
NULL, // 默认安全属性
NULL, // 默认堆栈大小
ThreadProc, // 线程入口地址(执行线程的函数)
(void*)&clnt_sock, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
clnt_socks[clnt_cnt++] = clnt_sock;
SetEvent(g_hEvent); /*设置受信*/
printf(" 接受到一个连接:%s 执行线程ID:%d\r\n", inet_ntoa(remoteAddr.sin_addr), dwThreadId);
}
WaitForMultipleObjects(clnt_cnt, hThread, true, INFINITE);
for (int i = 0; i < clnt_cnt; i++)
{
CloseHandle(hThread[i]);
}
// 关闭监听套节字
closesocket(serv_sock);
// 释放WS2_32库
WSACleanup();
return 0;
}
void error_handling(const char* msg)
{
printf("%s\n", msg);
WSACleanup();
exit(1);
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
int clnt_sock = *((int*)lpParam);
int str_len = 0, i;
char msg[BUF_SIZE];
while ((str_len = recv(clnt_sock, msg, sizeof(msg), 0)) != -1)
{
send_msg(msg, str_len);
printf("群发送成功\n");
}
printf("客户端退出:%d\n", GetCurrentThreadId());
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE);
for (i = 0; i < clnt_cnt; i++)
{
if (clnt_sock == clnt_socks[i])
{
while (i++ < clnt_cnt - 1)
clnt_socks[i] = clnt_socks[i + 1];
break;
}
}
clnt_cnt--;
SetEvent(g_hEvent); /*设置受信*/
// 关闭同客户端的连接
closesocket(clnt_sock);
return NULL;
}
void send_msg(char* msg, int len)
{
int i;
/*等待内核事件对象状态受信*/
WaitForSingleObject(g_hEvent, INFINITE);
for (i = 0; i < clnt_cnt; i++)
send(clnt_socks[i], msg, len, 0);
SetEvent(g_hEvent); /*设置受信*/
}
client.c
#include <winsock2.h>
#include <stdio.h>
#include <windows.h>
// 告诉连接器与WS2_32库连接
#pragma comment(lib,"WS2_32.lib")
#define BUF_SIZE 256
#define NAME_SIZE 30
DWORD WINAPI send_msg(LPVOID lpParam);
DWORD WINAPI recv_msg(LPVOID lpParam);
void error_handling(const char* msg);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main()
{
HANDLE hThread[2];
DWORD dwThreadId;
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
WSAStartup(sockVersion, &wsaData);
/*设置登录用户名*/
printf("Input your Chat Name:");
scanf("%s", name);
getchar(); //接收换行符
// 创建套节字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET)
error_handling("Failed socket()");
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(8888);
// 如果你的计算机没有联网,直接使用本地地址127.0.0.1
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (connect(sock, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
error_handling("Failed connect()");
printf("connect success\n");
hThread[0] = CreateThread(
NULL, // 默认安全属性
NULL, // 默认堆栈大小
send_msg, // 线程入口地址(执行线程的函数)
&sock, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
hThread[1] = CreateThread(
NULL, // 默认安全属性
NULL, // 默认堆栈大小
recv_msg, // 线程入口地址(执行线程的函数)
&sock, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
// 等待线程运行结束
WaitForMultipleObjects(2, hThread, true, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
printf(" Thread Over,Enter anything to over.\n");
getchar();
// 关闭套节字
closesocket(sock);
// 释放WS2_32库
WSACleanup();
return 0;
}
DWORD WINAPI send_msg(LPVOID lpParam)
{
int sock = *((int*)lpParam);
char name_msg[NAME_SIZE + BUF_SIZE];
while (1)
{
fgets(msg, BUF_SIZE, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
{
closesocket(sock);
exit(0);
}
sprintf(name_msg, "[%s]: %s", name, msg);
int nRecv = send(sock, name_msg, strlen(name_msg), 0);
}
return NULL;
}
DWORD WINAPI recv_msg(LPVOID lpParam)
{
int sock = *((int*)lpParam);
char name_msg[NAME_SIZE + BUF_SIZE];
int str_len;
while (1)
{
str_len = recv(sock, name_msg, NAME_SIZE + BUF_SIZE - 1, 0);
if (str_len == -1)
return -1;
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}
void error_handling(const char* msg)
{
printf("%s\n", msg);
WSACleanup();
exit(1);
}