导入
聊天室的架构还是常见的C/S架构,匿名聊天室的特点主要有三点:
-
匿名性:连接时分配随机ID(如”用户123”)
-
实时性:采用I/O多路复用技术
-
轻量级:单线程处理所有连接
为什么不采用多线程?因为多线程的开销很大,对于小项目无所谓,如果是针对“C10K”级别甚至以上的连接的大项目,多线程很难实现。即使采用线程池也是一样,线程池只是节省了线程创建和销毁的开销,但是节省不了线程切换的开销,而线程切换恰恰是线程模型性能损耗比较大的地方。
而多路IO复用,比如select,poll,epoll相比线程模型来说就轻量又实时,特点就是高并发非常擅长,因为是内核事件驱动。但是也有缺点,就是连接活跃度高,如果是消息传输频率非常高的场景,多线程或许更好,因为线程模型虽然并发低,但是连接活跃度很高。
实现
服务器端
服务器端只需要监听socket描述符,然后针对不同的事件做不同的处理,事件主要是POLLIN事件,代表输入或者说写事件,服务器端socket描述符发生写事件,代表新连接,接收新连接,分配匿名,加入监听描述符列表即可。客户端socket描述如发生POLLIN写事件代表客户端数据传入,服务器只需要接收消息,加上匿名前缀,然后广播即可。
#include <stdio.h>
#include <sys/socket.h>
#include <poll.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define MAXCLIENTS 10
#define PORT 8888
typedef struct {
int fd;
char name[16];
}Client;
Client clients[MAXCLIENTS];
void random_name(char *name){
const char charset[] = "abcdefghijklmnopqrstuvwxyz123456789";
for (int i=0;i<5;i++){
name[i] = charset[rand() % (sizeof(charset) - 1)];
}
name[5]='\0';
char prefix[] = "User-";
strcat(prefix, name);
strcpy(name, prefix);
}
int main()
{
srand(time(NULL));
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {AF_INET, htons(PORT), 0};
bind(sockfd,(struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, MAXCLIENTS);
struct pollfd fds[MAXCLIENTS + 1];
fds[0].fd = sockfd;
fds[0].events = POLLIN;
int nfds = 1;
while(1){
char buffer[256] = {0};
char msg[300] = {0};
int ready = poll(fds, nfds, 50000);
for(int i=0; i<nfds; ++i){
if (fds[i].revents & POLLIN){
if (fds[i].fd == sockfd){
int clientfd = accept(sockfd, NULL, NULL);
clients[nfds-1].fd = clientfd;
random_name(clients[nfds-1].name);
fds[nfds].fd = clientfd;
fds[nfds++].events = POLLIN;
sprintf(msg, "%s join in...", clients[nfds-2].name);
for (int j=1; j<nfds; ++j){
send(fds[j].fd, msg, 300, 0);
}
}else{
if (recv(fds[i].fd, buffer, 255, 0) <= 0){
sprintf(msg, "%s leave out...", clients[i-1].name);
close(fds[i].fd);
clients[i-1] = clients[nfds-2];
fds[i] = fds[--nfds];
for (int j=1; j<nfds; ++j){
send(fds[j].fd, msg, 300, 0);
}
break;
}
sprintf(msg, "%s:%s", clients[i-1].name, buffer);
for (int j=1; j<nfds; ++j){
send(fds[j].fd, msg, 300, 0);
}
}
}
}
}
return 0;
}
客户端
客户端就更简单,只需要监听stdin标准输入和服务器的socket描述符即可,标准输入发生POLLIN事件,代表你输入了消息,读取并把消息发给服务器即可,同样,服务器socket描述符发生了POLLIN事件,代表接收到服务器消息,直接打印在标准输出就行。
#include <stdio.h>
#include <sys/socket.h>
#include <poll.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {AF_INET, htons(PORT), 0};
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
struct pollfd fds[2];
fds[0].fd = sockfd;
fds[0].events = POLLIN;
fds[1].fd = 0;
fds[1].events = POLLIN;
int nfds = 2;
while(1){
char buffer[256] = {0};
int ready = poll(fds, nfds, 50000);
if (fds[1].revents & POLLIN){
read(0, buffer, 255);
send(sockfd, buffer, 255, 0);
}else if (fds[0].revents & POLLIN){
if (recv(sockfd, buffer, 255, 0) == 0){
return 0;
}
printf("%s\n", buffer);
}
}
return 0;
}
printf("%s\n", buffer);
}
}
return 0;
}
如果服务器端部署在公网IP下的服务器上,使用对应的公网ip即可,只是需要注意开放防火墙对应端口。
浏览器聊天室
既然写到匿名聊天时了,就想着更加实用一点,每个客户端都要一个专门可执行文件也不太方便,索性直接部署到浏览器上吧,浏览器直接访问,也更加方便。针对浏览器有比socket更加方便的协议,那就是websocket,websocket基于TCP协议,但是利用了HTTP协议的握手部分,所以可以兼容,但是websocket和HTTP除此之外并没有什么关系,一些读者可能会认为websocket协议是基于HTTP协议的,其实不然。
HTTP是请求响应形式的短连接,一次请求,一次响应,虽然后面的标准可以多次响应,但是还是属于短连接,而websocket是持久连接。HTTP数据推送是轮询的方式,是半双工,而websocket最大的特点是全双工,且服务器可以主动向客户端推送数据,而HTTP只能响应请求,无法主动推送。
websocket就简单介绍到这,利用websocket实现服务器大致是这样:
import asyncio
import websockets
import random
import json
connected_clients = {}
COLORS = ['#FF5733', '#33FF57', '#3357FF', '#F033FF', '#33FFF5']
async def handle_client(websocket, path):
username = f"用户{random.randint(1000,9999)}"
color = random.choice(COLORS)
connected_clients[websocket] = {"username": username, "color": color}
try:
async for message in websocket:
msg_data = {
"username": username,
"color": color,
"message": message
}
print(f"Received: {msg_data}")
await broadcast(json.dumps(msg_data))
finally:
del connected_clients[websocket]
async def broadcast(message):
if connected_clients:
tasks = [asyncio.create_task(client.send(message))
for client in connected_clients]
await asyncio.gather(*tasks)
start_server = websockets.serve(handle_client, "0.0.0.0", 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
利用python的asyncio异步配合websocket非常方便。
客户端因为浏览器访问,索然自然使用js来编写,且直接嵌入到html中,可以直接打开访问或者服务器利用nginx之类部署http服务 。
<DOCTYPE html>
<html>
<head>
<title>Chat Room</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 0 auto; }
#messages { height: 300px; border: 1px solid #ccc; overflow-y: scroll; padding: 10px; }
#messageInput { width: 80%; padding: 8px; }
#sendButton { padding: 8px 15px; }
.message { display: flex; margin: 5px 0; align-items: center; }
.avatar { width: 30px; height: 30px; border-radius: 50%; margin-right: 10px; }
.username { font-weight: bold; margin-right: 5px; }
</style>
</head>
<body>
<h1>匿名聊天室</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Type your message...">
<button id="sendButton">Send</button>
<script>
const ws = new WebSocket(`ws://${window.location.hostname}:8765`);
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
ws.onmessage = (event) => {
const msgData = JSON.parse(event.data);
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
const avatar = document.createElement('div');
avatar.className = 'avatar';
avatar.style.backgroundColor = msgData.color;
const username = document.createElement('span');
username.className = 'username';
username.textContent = msgData.username + ':';
const content = document.createElement('span');
content.textContent = msgData.message;
messageDiv.appendChild(avatar);
messageDiv.appendChild(username);
messageDiv.appendChild(content);
messages.appendChild(messageDiv);
messages.scrollTop = messages.scrollHeight;
};
sendButton.onclick = () => {
if (messageInput.value) {
ws.send(messageInput.value);
messageInput.value = '';
}
};
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendButton.click();
}
});
</script>
</body>
</html>
一个非常简单的html界面,配合js使用websocket监听键盘事件,然后发送给websocket服务器,服务器接收消息并且广播。
然后你就可以得到这样的效果:
总结
IO多路复用,以及websocket协议的特点和使用。