21、C++项目实战:聊天室程序(Socket编程)
聊到网络编程,很多人第一反应就是“难”。其实说白了,Socket编程就是让两台机器互相传数据。我当年第一次写聊天室时,也踩了不少坑。今天咱们就把它拆开揉碎了讲清楚。
一、聊天室程序的核心架构
一个聊天室程序,本质上就是“多对多”的通信模型。服务端负责转发消息,客户端负责收发。我个人习惯把这种架构叫做“星型模型”——所有客户端都连到中心服务器。
核心角色:
- 服务端:监听连接、管理客户端列表、转发消息
- 客户端:连接服务端、发送消息、接收消息
你想想看,如果每个客户端都直接连到其他客户端,那复杂度就爆炸了。所以中心服务器是必须的。
二、服务端实现:从零开始
服务端要做三件事:创建Socket、绑定端口、监听连接。嗯,这里要注意——端口号不要用1024以下的,那是系统保留的。我建议用8888或9999。
2.1 创建监听Socket
#include <winsock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main() {
// 初始化Winsock
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 创建Socket
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSock == INVALID_SOCKET) {
std::cerr << "创建Socket失败" << std::endl;
return 1;
}
// 绑定地址
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY;
bind(listenSock, (sockaddr*)&addr, sizeof(addr));
// 开始监听
listen(listenSock, SOMAXCONN);
std::cout << "服务器已启动,等待连接..." << std::endl;
// ... 后续处理
closesocket(listenSock);
WSACleanup();
return 0;
}
小提示:WSAStartup一定要在调用任何Socket函数之前执行。我曾经忘了这步,结果程序一直报错,排查了半天才发现。
2.2 接受客户端连接
服务端需要循环接受连接。每个新连接来了,就创建一个新线程去处理。我个人习惯用std::thread,简单直接。
#include <thread>
#include <vector>
std::vector<SOCKET> clients;
void HandleClient(SOCKET clientSock) {
char buffer[1024];
while (true) {
int bytes = recv(clientSock, buffer, sizeof(buffer), 0);
if (bytes <= 0) {
// 客户端断开
break;
}
buffer[bytes] = '\0';
// 广播给所有其他客户端
for (SOCKET s : clients) {
if (s != clientSock) {
send(s, buffer, bytes, 0);
}
}
}
// 清理
closesocket(clientSock);
}
int main() {
// ... 初始化代码同上
while (true) {
SOCKET clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET) {
clients.push_back(clientSock);
std::thread(HandleClient, clientSock).detach();
}
}
}
注意:多线程访问clients向量时,一定要加锁!否则多个线程同时push_back会导致崩溃。我建议用std::mutex保护。
三、客户端实现:简单直接
客户端就简单多了——连接服务器,然后开两个线程:一个收消息,一个发消息。
void ReceiveThread(SOCKET sock) {
char buffer[1024];
while (true) {
int bytes = recv(sock, buffer, sizeof(buffer), 0);
if (bytes <= 0) break;
buffer[bytes] = '\0';
std::cout << "收到: " << buffer << std::endl;
}
}
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr));
std::cout << "已连接到服务器" << std::endl;
std::thread recvThread(ReceiveThread, sock);
recvThread.detach();
// 主线程负责发送
std::string msg;
while (std::getline(std::cin, msg)) {
send(sock, msg.c_str(), msg.size(), 0);
}
closesocket(sock);
WSACleanup();
return 0;
}
四、避坑指南:我踩过的那些坑
做这个项目时,我遇到了不少问题。分享几个典型的:
- 粘包问题:TCP是流式协议,多条消息可能粘在一起。解决方案是自定义协议头,比如前4个字节表示消息长度。
- 客户端断开检测:recv返回0或SOCKET_ERROR时,一定要及时清理客户端列表。否则会一直尝试给断开的客户端发消息。
- 中文乱码:Windows下默认是GBK编码,Linux是UTF-8。跨平台通信时,建议统一用UTF-8。
核心知识点总结:
| 组件 | 关键函数 | 注意事项 |
|---|---|---|
| 服务端 | socket, bind, listen, accept | 端口不要冲突,多线程加锁 |
| 客户端 | socket, connect, send, recv | 处理断线重连 |
| 消息处理 | 自定义协议头 | 解决粘包问题 |
五、扩展思路:让聊天室更强大
基础功能实现了,还可以加一些实用特性:
- 私聊功能:消息格式改成 "目标用户名:内容",服务端解析后只转发给指定客户端。
- 房间系统:每个房间一个独立的客户端列表,互不干扰。
- 文件传输:用二进制模式发送文件数据,注意分包和重组。
我记得有一次给聊天室加了表情包功能,结果因为没处理好二进制数据,导致图片显示不全。后来加了CRC校验才搞定。
好了,Socket编程的核心思路就这些。动手写一个试试,遇到问题再回来翻翻这篇文章。代码跑通了,你就算真正入门网络编程了。