基于 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实现线程同步。
HANDLECreateEvent(LPSECURITY_ATTRIBUTESlpEventAttributes,//安全属性BOOLbManualReset,//是否手动重置事件对象为未受信对象BOOLbInitialState,//指定事件对象创建时的初始状态LPCSTRlpName//事件对象的名称);
设置内核对象状态
BOOLSetEvent(HANDLEhEvent/*设置事件内核对象受信*/);BOOLResetEvent(HANDLEhEvent/*设置事件内核对象未受信*/);
堵塞等待事件内核对象直到事件内核对象的状态为受信。
DWORDWaitForSingleObject(HANDLEhHandle,DWORDdwMilliseconds);
具体使用阅读全文在我的个人网站里看,篇幅太多。
服务端设计
在创建套接字绑定监听之后会有一个等待连接的过程,在接收到新连接之后,需要创建一个线程来处理新连接,当有多个新连接时可通过创建多个线程来处理新连接,
定义大连接数量以及大套接字和大线程
define MAX_CLNT 256intclnt_cnt=0;//统计套接字intclnt_socks[MAX_CLNT];//管理套接字HANDLEhThread[MAX_CLNT];//管理线程
当有新连接来临的时候创建线程处理新连接,并将新连接添加到套接字数组里面管理
hThread[clnt_cnt]=CreateThread(NULL,// 默认安全属性NULL,// 默认堆栈大小ThreadProc,// 线程入口地址(执行线程的函数)(void*)&clnt_sock,// 传给函数的参数0,// 指定线程立即运行&dwThreadId);// 返回线程的ID号clnt_socks[clnt_cnt++]=clnt_sock;
线程的处理函数ThreadProc不做讲解,大致是数据的收以及群发。
主要讲解线程同步,当有多个新连接来临的时候,可能会造成多个线程同时访问同一个数据(例如clnt_cnt)。这个时候需要线程的同步来避免破坏数据的完整性。
首先是创建一个内核事件
HANDLEg_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);/*设置受信*/
通过套接字数组来进行数据的转发实现群聊功能,此时也用到了线程同步
voidsend_msg(char*msg,intlen){inti;/*等待内核事件对象状态受信*/WaitForSingleObject(g_hEvent,INFINITE);for(i=0;i<clnt_cnt;i++)send(clnt_socks[i],msg,len,0);SetEvent(g_hEvent);/*设置受信*/}
客户端设计
同样也是在创建套接字连接到服务器之后,创建两个线程
一个和服务端进行数据的发送
DWORDWINAPIsend_msg(LPVOIDlpParam){intsock=*((int*)lpParam);charname_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);intnRecv=send(sock,name_msg,strlen(name_msg),0);}returnNULL;}
一个用来接收服务端数据并打印输出到终端
DWORDWINAPIrecv_msg(LPVOIDlpParam){intsock=*((int*)lpParam);charname_msg[NAME_SIZE+BUF_SIZE];intstr_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);}returnNULL;}
这样不会阻塞等待终端输入之后再显示服务端发送过来的消息了。
来源[源码获取]:
windows简易聊天室
链接:https://pan.baidu.com/s/1b5OnClTAICaygZ0lCoq9kg提取码:i6v3