<think>我们之前讨论过双人聊天的实现,现在需要扩展为多人聊天室,并添加公聊、私聊、上线通知、离线通知等功能,同时支持特定的键盘操作(Tab选择私聊对象,Enter确认私聊,ESC返回公聊)。由于是在Windows11上使用Dev-C++5.11开发,我们将使用Winsock库进行网络编程。基本设计思路:1.服务端:管理多个客户端的连接,负责消息的转发(包括公聊、私聊、通知等)。2.客户端:连接服务端,发送消息,接收来自服务端的消息,并实现键盘操作切换聊天模式。功能点:-公聊:发送给所有客户端-私聊:发送给指定客户端(由用户选择)-上线通知:当有新客户端连接时,通知所有客户端-离线通知:当有客户端断开连接时,通知所有客户端-键盘操作:Tab键循环选择私聊对象,Enter键确认私聊,ESC键返回公聊模式服务端流程:1.初始化Winsock库。2.创建监听套接字,绑定地址和端口,开始监听。3.使用一个列表(如vector或数组)存储所有连接的客户端套接字。4.循环接受客户端连接。每接受一个新的客户端,将其加入列表,并创建一个线程来处理该客户端的消息接收和转发。5.在服务端,每个客户端连接线程中,接收该客户端发来的消息,然后根据消息类型(公聊或私聊)进行转发。-如果消息是私聊,则消息格式可以设计为包含目标客户端的标识(如ID或用户名)和消息内容。-如果是公聊,则广播给所有客户端(除了自己)。6.同时,当有客户端断开时,从列表中移除,并通知所有客户端。客户端流程:1.初始化Winsock库。2.连接服务端。3.创建一个线程用于接收服务端发来的消息(包括其他客户端发来的消息和通知)。4.在主线程中,等待用户输入,并支持键盘操作来切换聊天模式。-默认公聊模式:用户输入消息后按Enter发送给所有人。-按Tab键:进入私聊模式,循环显示在线用户列表并选择其中一个作为私聊对象,然后按Enter确认,进入私聊输入状态。-在私聊模式下,用户输入消息后按Enter,消息只发送给选定的私聊对象。-按ESC键:退出私聊模式,返回公聊模式。5.发送消息时,需要将消息格式化为服务端可以识别的格式(如包含发送者、接收者、消息类型等)。消息格式设计(示例):我们可以设计一个结构体来表示消息,但由于网络传输需要,我们可能使用字符串来传递,因此定义一种简单的协议:例如:[类型]:[接收者(如果是私聊)]:[发送者]:[消息内容]但是为了简单,我们可以这样:-在客户端,当处于公聊模式时,发送的消息前加上一个前缀,如"PUBLIC:",然后直接发送。-当处于私聊模式时,发送的消息前加上"PRIVATE:[接收者用户名]:",然后发送。然而,更一般的是设计一个数据结构,然后序列化为字符串,但为了简便,我们使用字符串前缀。但是,考虑到上线通知和离线通知,我们还需要定义系统消息,例如:"SYSTEM:CLIENT_JOIN:[用户名]""SYSTEM:CLIENT_LEFT:[用户名]"服务端需要维护每个客户端的用户名(在客户端连接时,首先发送用户名进行登录)。因此,流程修改:1.客户端连接后,首先发送自己的用户名(作为第一条消息)。2.服务端收到用户名,将其与套接字关联,并广播上线通知。具体实现步骤:一、服务端步骤1:初始化Winsock步骤2:创建套接字,绑定,监听步骤3:维护客户端列表(包括套接字、用户名、IP地址等信息)步骤4:循环接受客户端连接步骤5:为每个客户端创建一个线程,用于接收该客户端发来的消息并处理在服务端的处理线程中:while(true){接收消息如果接收失败(recv返回值<=0),则关闭套接字,从列表中移除,并广播离线通知,然后退出线程。如果是首次消息(即用户名),则记录用户名,并广播上线通知。否则,解析消息类型:如果消息以"PUBLIC:"开头,则去掉前缀,然后将消息发送给所有客户端(除了自己),发送格式可以为:"[发送者用户名](所有人):消息内容"如果消息以"PRIVATE:"开头,则解析出目标用户名和消息内容,然后查找目标用户名对应的套接字,将消息发送给该套接字(发送格式为"[发送者用户名](私聊):消息内容"),同时也要发给发送者自己(以便在发送方界面显示)?或者只发送给目标用户?可以根据需求设计。通常私聊消息只发送给目标用户,但也可以同时返回给发送者(这样发送者自己也能看到)。我们设计为:私聊消息同时发送给发送者和接收者,这样双方都能看到。格式为:"[发送者用户名]私聊[接收者用户名]:消息内容"(或者更简单的"[发送者用户名](私聊)>[接收者用户名]:消息内容")注意:系统消息由服务端发送,格式统一为"SYSTEM:消息内容",客户端接收后显示为系统通知。二、客户端1.初始化Winsock,创建套接字,连接服务器。2.首先发送用户名(用户需要在启动时输入用户名?或者程序自动生成?这里我们假设用户在客户端启动时输入用户名)。3.创建一个线程用于接收服务端发来的消息,并显示在聊天窗口中。4.主线程用于获取用户输入,并处理键盘事件。但是,在控制台程序中,如何实现非阻塞的键盘输入监听?我们需要使用一些方法,例如:使用_kbhit()和_getch()(conio.h)来非阻塞获取键盘输入。客户端主线程循环:while(true){使用_kbhit()检查是否有按键,然后根据当前模式处理:-公聊模式:按Tab:进入私聊选择模式,显示在线用户列表,并让用户通过Tab循环选择,Enter确认,ESC取消返回。其他:输入消息,按Enter发送。-私聊模式(选择目标阶段):按Tab:循环选择下一个在线用户按Enter:确认选择,然后进入私聊输入状态按ESC:取消选择,返回公聊模式-私聊输入状态(已选择目标):输入消息,按Enter发送(发送给选定目标)按ESC:返回公聊模式,取消私聊目标为了简化,我们可以这样设计状态:enumChatMode{Public,Private_Selecting,Private_Chatting};ChatModecurrentMode=Public;stringprivateTarget;//当前私聊对象在主循环中,如果当前不是私聊输入状态,则允许使用Tab键进入私聊选择模式。一旦在Private_Selecting状态下按Enter,就记录选中的用户,并进入Private_Chatting状态,此时用户输入的消息都是私聊消息,直到按ESC退出私聊(回到公聊)。但是,在Private_Chatting状态下,用户输入消息时,我们也可以支持按ESC取消当前私聊(回到公聊)?或者按Tab重新选择?这里按需求设计:按ESC退出私聊模式,按Tab重新选择私聊对象。重新设计键盘操作:-公聊模式下,按Tab进入私聊选择模式,此时在控制台上显示在线用户列表(通过服务端定期发送?或者当用户列表变化时更新?这里我们需要客户端维护一个在线用户列表)。在私聊选择模式下,按Tab切换选中的用户,按Enter确认选择(进入私聊输入模式),按ESC返回公聊模式。-私聊输入模式下,输入消息后按Enter发送,按ESC返回公聊模式,按Tab可以重新选择私聊对象(此时需要进入私聊选择模式,但保留当前私聊目标?或者清空?)。这里我们设计:按ESC在任何私聊阶段都会返回公聊模式。在私聊输入模式下,按Tab可以重新选择私聊对象(即进入私聊选择模式),同时保留当前的私聊目标作为当前选中的对象(如果重新选择,则改变私聊目标)。需要客户端维护一个在线用户列表(包括自己?一般不包括)。这个列表如何获得?服务端在每次用户加入或离开时,广播给所有客户端最新的在线用户列表(作为系统消息的一种),或者客户端在连接成功时,服务端发送当前在线用户列表,并在每次变化时发送更新。我们设计一个系统消息:当有用户加入或离开时,服务端广播更新用户列表。客户端收到后更新本地列表。消息示例:系统消息更新用户列表:SYSTEM:USERLIST:user1,user2,user3或者分别发送:当有用户加入时,除了发送CLIENT_JOIN,再发送最新的用户列表?这样客户端就可以维护一个当前在线用户列表。但是,为了简单,我们在服务端每次用户加入或离开时,都广播一次最新的用户列表(所有用户名)。这样客户端只需要解析该消息并更新本地列表即可。三、键盘操作实现在客户端主循环中,我们需要非阻塞地检测键盘输入。可以使用conio.h中的_kbhit和_getch。代码片段:#include<conio.h>if(_kbhit()){charch=_getch();//处理按键}但是,要区分Tab、Enter、ESC这些特殊键,它们都是扩展键(两个字节,第一个是0或0xE0,第二个才是具体键值)。ESC的键值是27,而Tab键值是9,Enter键值是13(但有时候是\r,即13,但作为扩展键,Enter是第一个字节为0,第二个字节为0x0D,即13?)在Windows下,使用_getch读取扩展键时,会先返回0或0xE0,然后再返回具体键码。实际处理:charch=_getch();if(ch==0||ch==0xE0){ch=_getch();//取第二个字节//此时ch是扩展键码}扩展键码参考:Tab:没有扩展?直接是9(普通键值)ESC:27(普通键值)Enter:13(普通键值)?但是控制台里按Enter是13(\r)?实际上,在控制台中,Enter键会返回两个字符:\r(13)和\n(10),但是使用_getch()会返回13(并且后续的换行处理会给出10,但_getch只返回一个)。因此,我们可以直接处理。具体:Tab:9Enter:13ESC:27因此,在非阻塞检测中,我们这样处理:if(ch==9){//Tab//处理Tab}elseif(ch==13){//Enter//处理Enter}elseif(ch==27){//ESC//处理ESC}四、代码结构示例由于代码量较大,这里只给出关键部分的伪代码和部分实现。服务端伪代码:```c#include<winsock2.h>#include<iostream>#include<vector>#include<string>#include<algorithm>#pragmacomment(lib,"ws2_32.lib")usingnamespacestd;structClientInfo{SOCKETsock;stringusername;//其他信息};vector<ClientInfo>clients;voidBroadcast(stringmessage,SOCKETexclude=INVALID_SOCKET);//广播消息,除了exclude套接字voidSendUserList();//广播在线用户列表DWORDWINAPIClientThread(LPVOIDlpParam){ClientInfo*client=(ClientInfo*)lpParam;SOCKETclientSock=client->sock;charbuffer[1024];intbytesReceived;boolisFirst=true;while(true){bytesReceived=recv(clientSock,buffer,sizeof(buffer),0);if(bytesReceived<=0){//客户端断开cout<< "Clientdisconnected"<< endl;closesocket(clientSock);//从列表中移除autoit=find_if(clients.begin(),clients.end(),[client](constClientInfo&c){returnc.sock==client->sock;});if(it!=clients.end()){clients.erase(it);//广播该用户离开if(!client->username.empty()){stringmsg="SYSTEM:CLIENT_LEFT:"+client->username;Broadcast(msg);SendUserList();//更新用户列表}}deleteclient;return0;}buffer[bytesReceived]='\0';stringrecvMsg=buffer;if(isFirst){//首次消息,作为用户名client->username=recvMsg;cout<< client->username<< "joined!"<< endl;isFirst=false;//广播该用户加入stringmsg="SYSTEM:CLIENT_JOIN:"+client->username;Broadcast(msg);SendUserList();//更新用户列表}else{//处理消息if(recvMsg.find("PUBLIC:")==0){stringpublicMsg=recvMsg.substr(7);stringsendMsg="PUBLIC:"+client->username+":"+publicMsg;Broadcast(sendMsg,client->sock);//不排除自己,这样自己也能看到自己发的公聊消息(在广播函数里排除也可以,这里我们不排除)//如果不想自己看到,则排除自己}elseif(recvMsg.find("PRIVATE:")==0){//格式:PRIVATE:目标用户名:消息内容stringmsgContent=recvMsg.substr(8);intpos=msgContent.find(':');if(pos!=string::npos){stringtargetUser=msgContent.substr(0,pos);stringprivateMsg=msgContent.substr(pos+1);stringsendMsg="PRIVATE:"+client->username+">"+targetUser+":"+privateMsg;//将消息发送给目标用户和发送者自己(这样双方都能看到)for(auto&c:clients){if(c.username==targetUser||c.username==client->username){send(c.sock,sendMsg.c_str(),sendMsg.size(),0);}}}}}}}intmain(){//初始化WinsockWSADATAwsa;WSAStartup(MAKEWORD(2,2),&wsa);//创建套接字SOCKETlistenSock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);sockaddr_inaddr;addr.sin_family=AF_INET;addr.sin_addr.s_addr=INADDR_ANY;addr.sin_port=htons(8888);bind(listenSock,(sockaddr*)&addr,sizeof(addr));listen(listenSock,SOMAXCONN);while(true){sockaddr_inclientAddr;intaddrLen=sizeof(clientAddr);SOCKETclientSock=accept(listenSock,(sockaddr*)&clientAddr,&addrLen);ClientInfo*client=newClientInfo;client->sock=clientSock;//新客户端加入cout<< "Newclientconnected:"<< inet_ntoa(clientAddr.sin_addr)<< endl;clients.push_back(*client);//注意:这里存的是副本,后面线程中用的是堆上的client//创建线程CreateThread(NULL,0,ClientThread,(LPVOID)client,0,NULL);}return0;}voidBroadcast(stringmessage,SOCKETexclude){for(auto&client:clients){if(exclude==INVALID_SOCKET||client.sock!=exclude){send(client.sock,message.c_str(),message.size(),0);}}}voidSendUserList(){stringuserList="SYSTEM:USERLIST:";for(inti=0;i<clients.size();i++){userList+=clients[i].username;if(i< clients.size()-1)userList+=",";}Broadcast(userList);}```客户端代码结构:客户端需要两个线程:一个用于接收消息,一个用于发送消息(但发送消息在主线程中,接收消息在另一个线程中)。伪代码:```c#include<winsock2.h>#include<iostream>#include<string>#include<conio.h>#include<vector>#pragmacomment(lib,"ws2_32.lib")usingnamespacestd;vector<string>onlineUsers;//在线用户列表stringmyUsername;SOCKETsock;boolrunning=true;enumChatMode{MODE_PUBLIC,MODE_PRIVATE_SELECTING,MODE_PRIVATE_CHATTING};ChatModecurrentMode=MODE_PUBLIC;intselectedUserIndex=-1;//当前在私聊选择模式下选中的用户索引DWORDWINAPIRecvThread(LPVOIDlpParam){charbuffer[1024];while(running){intbytesRecv=recv(sock,buffer,sizeof(buffer)-1,0);if(bytesRecv<=0){cout<< "Disconnectedfromserver"<< endl;running=false;break;}buffer[bytesRecv]='\0';stringmsg=buffer;//处理接收到的消息//1.系统消息:SYSTEM:if(msg.find("SYSTEM:")==0){stringsysMsg=msg.substr(7);if(sysMsg.find("USERLIST:")==0){stringuserListStr=sysMsg.substr(9);//更新在线用户列表onlineUsers.clear();//按逗号分割size_tpos=0;while((pos=userListStr.find(','))!=string::npos){stringuser=userListStr.substr(0,pos);onlineUsers.push_back(user);userListStr.erase(0,pos+1);}if(!userListStr.empty()){onlineUsers.push_back(userListStr);}//显示用户列表更新cout<< "\n===在线用户更新==="<< endl;for(auto&u:onlineUsers){cout<< u<< endl;}cout<< "================="<< endl;}elseif(sysMsg.find("CLIENT_JOIN:")==0){stringusername=sysMsg.substr(12);cout<< "\n系统消息:"<< username<< "加入了聊天室"<< endl;}elseif(sysMsg.find("CLIENT_LEFT:")==0){stringusername=sysMsg.substr(12);cout<< "\n系统消息:"<< username<< "离开了聊天室"<< endl;}}//2.公聊消息:PUBLIC:发送者:消息内容elseif(msg.find("PUBLIC:")==0){stringcontent=msg.substr(7);intpos=content.find(':');if(pos!=string::npos){stringsender=content.substr(0,pos);stringmsgContent=content.substr(pos+1);cout<< "\n["<< sender<< "](公聊):"<< msgContent<< endl;}}//3.私聊消息:PRIVATE:发送者>接收者:消息内容elseif(msg.find("PRIVATE:")==0){stringcontent=msg.substr(8);intpos1=content.find('>');intpos2=content.find(':');if(pos1!=string::npos&&pos2!=string::npos&&pos1< pos2){stringsender=content.substr(0,pos1);stringreceiver=content.substr(pos1+1,pos2-pos1-1);stringmsgContent=content.substr(pos2+1);//如果自己是接收者或者发送者,则显示if(receiver==myUsername||sender==myUsername){cout<< "\n["<< sender<< "私聊"<< receiver<< "]:"<< msgContent<< endl;}}}//显示提示符cout<< ">";}return0;}intmain(){cout<< "Enteryourusername:";cin>>myUsername;//初始化WinsockWSADATAwsa;WSAStartup(MAKEWORD(2,2),&wsa);sock=socket(AF_INET,SOCK_STREAM,0);sockaddr_inserverAddr;serverAddr.sin_family=AF_INET;serverAddr.sin_port=htons(8888);serverAddr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务端IPif(connect(sock,(sockaddr*)&serverAddr,sizeof(serverAddr)){cout<< "Connectfailed"<< endl;return1;}//首先发送用户名send(sock,myUsername.c_str(),myUsername.size(),0);//创建接收线程CreateThread(NULL,0,RecvThread,NULL,0,NULL);//主循环stringmessage;vector<string>inputBuffer;//用于保存输入中的多行?我们每次输入一行cout<< "Connected.Typeyourmessage(Tab:selectuser,ESC:backtopublic)"<< endl;//非阻塞键盘输入while(running){if(_kbhit()){charch=_getch();if(ch==9){//Tabif(currentMode==MODE_PUBLIC){currentMode=MODE_PRIVATE_SELECTING;selectedUserIndex=0;//显示在线用户列表,并让用户选择cout<< "\n选择私聊对象(Tab切换,Enter确认,ESC取消)"<< endl;for(inti=0;i<onlineUsers.size();i++){if(i==selectedUserIndex){cout<< ">"<< onlineUsers[i]<< endl;}else{cout<< ""<< onlineUsers[i]<< endl;}}}elseif(currentMode==MODE_PRIVATE_SELECTING){selectedUserIndex=(selectedUserIndex+1)%onlineUsers.size();//重新显示用户列表system("cls");//或者用其他方式清屏?这里为了简单使用cls(Windows)cout<< "\n选择私聊对象(Tab切换,Enter确认,ESC取消)"<< endl;for(inti=0;i<onlineUsers.size();i++){if(i==selectedUserIndex){cout<< ">"<< onlineUsers[i]<< endl;}else{cout<< ""<< onlineUsers[i]<< endl;}}}elseif(currentMode==MODE_PRIVATE_CHATTING){//在私聊输入模式下按Tab,重新进入选择模式currentMode=MODE_PRIVATE_SELECTING;//显示选择列表(同上)cout<< "\n选择私聊对象(Tab切换,Enter确认,ESC取消)"<< endl;for(inti=0;i<onlineUsers.size();i++){if(i==selectedUserIndex){cout<< ">"<< onlineUsers[i]<< endl;}else{cout<< ""<< onlineUsers[i]<< endl;}}}}elseif(ch==13){//Enterif(currentMode==MODE_PUBLIC){//在公聊模式下,按Enter会发送公聊消息stringinput;cout<< "请输入公聊消息:";getline(cin,input);//但是这里会阻塞?所以我们需要在非阻塞循环中处理//非阻塞输入如何实现整行输入?这比较复杂,我们可以将输入缓冲区自己管理,每次读取一个字符,然后组成一行。//由于这是一个简化示例,我们暂时使用阻塞输入(但实际上会影响接收线程的消息显示)//改进:使用非阻塞方式逐字符读取并组成行。stringmsg="PUBLIC:"+input;send(sock,msg.c_str(),msg.size(),0);}elseif(currentMode==MODE_PRIVATE_SELECTING){//在私聊选择模式下按Enter,进入私聊输入状态,目标用户是selectedUserIndex对应的用户currentMode=MODE_PRIVATE_CHATTING;cout<< "与["<< onlineUsers[selectedUserIndex]<< "]私聊中(按ESC返回公聊)"<< endl;}elseif(currentMode==MODE_PRIVATE_CHATTING){stringinput;cout<< "请输入私聊消息:";getline(cin,input);stringmsg="PRIVATE:"+onlineUsers[selectedUserIndex]+":"+input;send(sock,msg.c_str(),msg.size(),0);}}elseif(ch==27){//ESCif(currentMode!=MODE_PUBLIC){currentMode=MODE_PUBLIC;selectedUserIndex=-1;cout<< "切换至公聊模式"<< endl;}}else{//其他按键处理//如果当前是输入消息状态,则将字符加入到消息缓冲区}}//少量睡眠,避免CPU占用过高Sleep(100);}//清理closesocket(sock);WSACleanup();return0;}```注意:上述代码仅为示例,实际应用中需要处理更多细节,如错误处理、输入缓冲、界面刷新等。由于在控制台同时进行非阻塞输入和输出,可能会导致输出消息和用户输入混合,因此需要更复杂的控制台界面管理(如使用光标定位刷新)。但为了简化,我们每次输出都换行。以上代码在Dev-C++5.11中应该是可以编译的,注意在项目设置中链接ws2_32库。最后,关于Windows1164位系统适配:Winsock2是Windows系统通用的,所以可以正常运行。由于代码较长,用户需要自行调试。这个示例提供了一个基本框架,但实际开发中可能遇到各种问题,需要逐步完善。如果有进一步的问题,欢迎继续提问。生成相关问题:1.如何解决控制台程序中多线程输出的混乱问题(如接收线程输出消息打断用户输入)?2.如何实现私聊消息的历史记录?3.如何加密聊天内容以保证安全性?</think>
### 基于Socket的多人聊天室实现方案 (Dev-C++ 5.11/Win11)
以下是使用Dev-C++ 5.11在Windows 11平台上实现多人聊天室的核心步骤:
#### 一、系统架构设计
```mermaid
graph TD
A[客户端1] -->|TCP/IP| C[服务端]
B[客户端2] -->|TCP/IP| C
D[客户端n] -->|TCP/IP| C
C -->|广播通知| A
C -->|私聊转发| B
C -->|用户列表| D
```
#### 二、服务端实现 (`server.cpp`)
1. **初始化Winsock库**
```c
#include <winsock2.h>
#include <vector>
#include <map>
#pragma comment(lib, "ws2_32.lib")
#define PORT 8888
std::vector<SOCKET> clients; // 所有客户端
std::map<SOCKET, std::string> clientNames; // 套接字-用户名映射
```
2. **客户端线程处理函数**
```c
DWORD WINAPI ClientHandler(LPVOID lpParam) {
SOCKET client = *(SOCKET*)lpParam;
char buffer[1024];
int bytesRecv;
// 接收用户名作为首次消息
bytesRecv = recv(client, buffer, sizeof(buffer), 0);
std::string username(buffer, bytesRecv);
clientNames[client] = username;
// 广播上线通知
std::string joinMsg = "[SYSTEM] " + username + " 上线了!";
Broadcast(joinMsg.c_str(), client);
while (true) {
bytesRecv = recv(client, buffer, sizeof(buffer), 0);
if (bytesRecv <= 0) break; // 客户端断开
std::string msg(buffer, bytesRecv);
// 消息格式: [类型][接收者]:内容
if (msg[0] == 'P') { // 私聊 @username:message
int pos = msg.find(':', 1);
std::string target = msg.substr(1, pos-1);
std::string content = msg.substr(pos+1);
SendPrivate(client, target, content);
} else { // 公聊
Broadcast(("[" + username + "] " + msg.substr(1)).c_str());
}
}
// 处理离线
closesocket(client);
RemoveClient(client);
std::string leaveMsg = "[SYSTEM] " + username + " 离线了!";
Broadcast(leaveMsg.c_str());
return 0;
}
```
3. **核心辅助函数**
```c
// 广播消息 (排除发送者)
void Broadcast(const char* msg, SOCKET exclude = INVALID_SOCKET) {
for (SOCKET client : clients) {
if (client != exclude)
send(client, msg, strlen(msg), 0);
}
}
// 私聊发送
void SendPrivate(SOCKET sender, const std::string& target, const std::string& msg) {
std::string senderName = clientNames[sender];
std::string fullMsg = "[私聊][" + senderName + "->你] " + msg;
for (auto& [client, name] : clientNames) {
if (name == target) {
send(client, fullMsg.c_str(), fullMsg.length(), 0);
send(sender, ("[私聊][你->" + target + "] " + msg).c_str(), ...);
return;
}
}
// 目标不存在
send(sender, ("[错误] 用户 " + target + " 不存在").c_str(), ...);
}
```
#### 三、客户端实现 (`client.cpp`)
1. **键盘交互系统**
```c
enum ChatMode { PUBLIC, PRIVATE_TARGETING };
ChatMode currentMode = PUBLIC;
std::string targetUser; // 当前私聊对象
void HandleKeyboard() {
if (_kbhit()) {
int ch = _getch();
switch (ch) {
case 9: // TAB键
if (currentMode == PUBLIC) {
currentMode = PRIVATE_TARGETING;
ShowUserList();
} else {
CycleUsers(); // 循环选择用户
}
break;
case 13: // ENTER键
if (currentMode == PRIVATE_TARGETING) {
ConfirmPrivate();
}
break;
case 27: // ESC键
currentMode = PUBLIC;
targetUser.clear();
break;
}
}
}
```
2. **用户选择界面**
```c
void CycleUsers() {
static int index = 0;
index = (index + 1) % clientNames.size();
auto it = clientNames.begin();
std::advance(it, index);
targetUser = it->second;
// 控制台高亮显示选中用户
printf("\n>> 私聊对象: %s", targetUser.c_str());
}
void ConfirmPrivate() {
if (!targetUser.empty()) {
printf("\n进入私聊模式 [%s] (ESC退出)", targetUser.c_str());
}
}
```
3. **网络消息处理线程**
```c
DWORD WINAPI RecvThread(LPVOID lpParam) {
SOCKET server = *(SOCKET*)lpParam;
char buffer[1024];
int bytesRecv;
while (true) {
bytesRecv = recv(server, buffer, sizeof(buffer), 0);
if (bytesRecv <= 0) break;
// 系统消息解析
if (strncmp(buffer, "[SYSTEM]", 8) == 0) {
HandleSystemMsg(buffer);
}
// 更新用户列表
else if (strncmp(buffer, "@USERLIST@", 10) == 0) {
UpdateUserList(buffer);
}
else {
printf("\n%s", buffer);
}
}
return 0;
}
```
#### 四、Windows 11适配要点
1. **项目配置**
- 在Dev-C++中:Tools > Compiler Options >
```
Add -lws2_32 to linker command line
```
- 防火墙设置:允许`server.exe`和`client.exe`通过防火墙
2. **兼容性处理**
- 使用Winsock 2.2:`WSAStartup(MAKEWORD(2,2), &wsa)`
- 64位支持:Project > Project Options > General >
```
选择"64-bit Windows Console"
```
#### 五、核心功能测试流程
1. 启动服务端 → 2. 启动客户端A(输入用户名)→ 3. 启动客户端B → 4. A按Tab选择B → 5. A输入私聊消息 → 6. B按ESC返回公聊
> 注意:在局域网测试时使用真实IP,本机测试使用`127.0.0.1`
[^1]: 该设计基于Socket基础通信模型扩展,参考了双人通信的核心转发机制并添加了多人管理功能
---
###