原文链接:点我获取
基于 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
,具体案例这里不多做介绍.
详细案例参考 Windows核心编程之多进程多线程
每个线程都可以访问进程中的公共变量,资源,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。数据之间的相互制约包括
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
);
具体使用参考 windows核心编程之线程同步
在创建套接字绑定监听之后会有一个等待连接的过程,在接收到新连接之后,需要创建一个线程来处理新连接,当有多个新连接时可通过创建多个线程来处理新连接,
定义最大连接数量以及最大套接字和最大线程
#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下的网络编程以及多线程编程和线程的同步方法就可以实现这个样一个功能。
源代码关注微信公众号【编程学习基地】后台发送关键字[C语言聊天室获]取,socket网络编程方法可通过上期C语言实现web服务器学习。