自从开始学linux网络编程后就想写个聊天室,一开始原本打算用多进程的方式来写,可是发觉进程间的通信有点麻烦,而且开销也大,后来想用多线程能不能实现呢,于是便去看了一下linux里线程的用法,实际上只需要知道 pthread_create 就差不多了,于是动手开干,用了两天时间,调试的过程挺痛苦的,一开始打算用纯C来撸,便用简单的数组来存储客户端的连接信息,可是运行时出现了一些很奇怪的问题,不知道是不是访问了临界资源,和线程间的互斥有关等等;奇怪的是,当改用STL的set或map时问题就解决了,但上网搜了下发现STL也不是线程安全的,至于到底是什么问题暂时不想去纠结了,可能是其它一些小细节的错误吧。先贴上代码:
首先是必要的头文件 header.h:
#ifndef __HEADER_H #define __HEADER_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <error.h> #include <signal.h> #include <sys/wait.h> #include <assert.h> #include <pthread.h> #define bool int // the 3 lines is for c originally #define true 1 #define false 0 #define PORT 9003 #define BUF_LEN 1024 // 缓冲区大小 #define MAX_CONNECTION 6 // 服务器允许的最大连接数,可自行更改 #define For(i,s,t) for(i = (s); i != (t); ++i) #endif // __HEADER_H
然后是客户端部分 client.cpp,相对来说简单一些:
#include "header.h" // 客户端接收消息的线程函数 void* recv_func(void *args) { char buf[BUF_LEN]; int sock_fd = *(int*)args; while(true) { int n = recv(sock_fd, buf, BUF_LEN, 0); if(n <= 0) break; // 这句很关键,一开始不知道可以用这个来判断通信是否结束,用了其它一些很奇葩的做法来结束并关闭 sock_fd 以避免 CLOSE_WAIT 和 FIN_WAIT2 状态的出现T.T write(STDOUT_FILENO, buf, n); } close(sock_fd); exit(0); } // 客户端和服务端进行通信的处理函数 void process(int sock_fd) { pthread_t td; pthread_create(&td, NULL, recv_func, (void*)&sock_fd); // 新开个线程来接收消息,避免了一读一写的原始模式,一开始竟把它放进 while 循环里面了,泪崩。。。 char buf[BUF_LEN]; while(true) { int n = read(STDIN_FILENO, buf, BUF_LEN); buf[n++] = '\0'; // 貌似标准读入不会有字符串结束符的,需要自己手动添加 send(sock_fd, buf, n, 0); } close(sock_fd); } int main(int argc, char *argv[]) { assert(argc == 2); struct sockaddr_in cli; bzero(&cli, sizeof(cli)); cli.sin_family = AF_INET; cli.sin_addr.s_addr = htonl(INADDR_ANY); cli.sin_port = htons(PORT); // 少了 htons 的话就连接不上了,因为小端机器的原因??? int sc = socket(AF_INET, SOCK_STREAM, 0); if(sc < 0) { perror("socket error"); exit(-1); } inet_pton(AF_INET, argv[1], &(cli.sin_addr)); // 用第一个参数作为连接服务器端的地址 int err = connect(sc, (struct sockaddr*)&cli, sizeof(cli)); if(err < 0) { perror("connect error"); exit(-2); } process(sc); close(sc); return 0; }
最后是服务端 server.cpp:
#include <map> #include "header.h" using std::map; map<int, struct sockaddr_in*> socks; // 用于记录各个客户端,键是与客户端通信 socket 的文件描述符,值是对应的客户端的 sockaddr_in 的信息 // 群发消息给 socks 中的所有客户端 inline void send_all(const char *buf, int len) { for(auto it = socks.begin(); it != socks.end(); ++it) send(it->first, buf, len, 0); } // 服务端端接收消息的线程函数 void* recv_func(void* args) { int cfd = *(int*)args; char buf[BUF_LEN]; while(true) { int n = recv(cfd, buf, BUF_LEN, 0); if(n <= 0) break; // 关键的一句,用于作为结束通信的判断 write(STDOUT_FILENO, buf, n); if(strcmp(buf, "bye\n") == 0) { // 如果接收到客户端的 bye,就结束通信并从 socks 中删除相应的文件描述符,动态申请的空间也应在删除前释放 printf("close connection with client %d.\n", cfd); free(socks[cfd]); socks.erase(cfd); break; } send_all(buf, n); // 群发消息给所有已连接的客户端 } close(cfd); // 关闭与这个客户端通信的文件描述符 } // 和某一个客户端通信的线程函数 void* process(void *argv) { pthread_t td; pthread_create(&td, NULL, recv_func, (void*)argv); // 在主处理函数中再新开一个线程用于接收该客户端的消息 int sc = *(int*)argv; char buf[BUF_LEN]; while(true) { int n = read(STDIN_FILENO, buf, BUF_LEN); buf[n++] = '\0'; // 和客户端一样需要自己手动添加字符串结束符 send_all(buf, n); // 服务端自己的信息输入需要发给所有客户端 } close(sc); } int main(int argc, char *argv[]) { struct sockaddr_in serv; bzero(&serv, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(PORT); int ss = socket(AF_INET, SOCK_STREAM, 0); if(ss < 0) { perror("socket error"); return 1; } int err = bind(ss, (struct sockaddr*)&serv, sizeof(serv)); if(err < 0) { perror("bind error"); return 2; } err = listen(ss, 2); if(err < 0) { perror("listen error"); return 3; } socks.clear(); // 清空 map socklen_t len = sizeof(struct sockaddr); while(true) { struct sockaddr_in *cli_addr = (struct sockaddr_in*)malloc(sizeof(struct sockaddr_in)); int sc = accept(ss, (struct sockaddr*)cli_addr, &len); if(sc < 0) { free(cli_addr); continue; } if(socks.size() >= MAX_CONNECTION) { // 当将要超过最大连接数时,就让那个客户端先等一下 char buf[128] = "connections is too much, please waiting...\n"; send(sc, buf, strlen(buf) + 1, 0); close(sc); free(cli_addr); continue; } socks[sc] = cli_addr; // 指向对应申请到的 sockaddr_in 空间 printf("client %d connect me...\n", sc); pthread_t td; pthread_create(&td, NULL, process, (void*)&sc); // 开一个线程来和 accept 的客户端进行交互 } return 0; }
makefile文件:
all: server client
server: server.cpp
g++ -std=c++11 -o server server.cpp -lpthread
client: client.cpp
g++ -std=c++11 -o client client.cpp -lpthread
clean:
rm -f *.o
在我的ubuntu 14.04 64 位的机器上测试过没有什么问题,客户端与服务端能正常的交互和退出,能通过服务端接收其它客户端发送的消息,运行时cpu和内存占用情况正常,不会产生什么奇怪的bug。暂时只写了个终端的界面,客户端的UI迟点再去弄吧~
*****************************************************************************************************************************************
今天试了下用 PyQt4 去写个客户端的界面,调了好一天,总算能看到点东西了,先上图:
而命令行下的客户端(上面的 client.cpp 文件)的运行界面是这样子的:
服务端的运行情况是:
PyQt4 编写的客户端(pyqt_client.py)代码是:
#!/usr/bin/env python #-*- coding: utf-8 -*- from PyQt4 import QtGui, QtCore import sys import socket import thread class Client(QtGui.QWidget): BUF_LEN = 1024 def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) self.setWindowTitle(u'TCP客户端') self.resize(600, 500) self.center() layout = QtGui.QGridLayout(self) label_ip = QtGui.QLabel(u'远程主机IP:') layout.addWidget(label_ip, 0, 0, 1, 1) self.txt_ip = QtGui.QLineEdit('127.0.0.1') layout.addWidget(self.txt_ip, 0, 1, 1, 3) label_port = QtGui.QLabel(u'端口:') layout.addWidget(label_port, 0, 4, 1, 1) self.txt_port = QtGui.QLineEdit('9003') layout.addWidget(self.txt_port, 0, 5, 1, 3) self.isConnected = False self.btn_connect = QtGui.QPushButton(u'连接') self.connect(self.btn_connect, QtCore.SIGNAL( 'clicked()'), self.myConnect) layout.addWidget(self.btn_connect, 0, 8, 1, 2) label_recvMessage = QtGui.QLabel(u'消息内容:') layout.addWidget(label_recvMessage, 1, 0, 1, 1) self.btn_clearRecvMessage = QtGui.QPushButton(u'↓ 清空消息框') self.connect(self.btn_clearRecvMessage, QtCore.SIGNAL( 'clicked()'), self.myClearRecvMessage) layout.addWidget(self.btn_clearRecvMessage, 1, 7, 1, 3) self.txt_recvMessage = QtGui.QTextEdit() self.txt_recvMessage.setReadOnly(True) self.txt_recvMessage.setStyleSheet('background-color:yellow') layout.addWidget(self.txt_recvMessage, 2, 0, 1, 10) lable_name = QtGui.QLabel(u'姓名(ID):') layout.addWidget(lable_name, 3, 0, 1, 1) self.txt_name = QtGui.QLineEdit() layout.addWidget(self.txt_name, 3, 1, 1, 3) self.isSendName = QtGui.QRadioButton(u'发送姓名') self.isSendName.setChecked(False) layout.addWidget(self.isSendName, 3, 4, 1, 1) label_sendMessage = QtGui.QLabel(u' 输入框:') layout.addWidget(label_sendMessage, 4, 0, 1, 1) self.txt_sendMessage = QtGui.QLineEdit() self.txt_sendMessage.setStyleSheet("background-color:cyan") layout.addWidget(self.txt_sendMessage, 4, 1, 1, 7) self.btn_send = QtGui.QPushButton(u'发送') self.connect(self.btn_send, QtCore.SIGNAL('clicked()'), self.mySend) layout.addWidget(self.btn_send, 4, 8, 1, 2) self.btn_clearSendMessage = QtGui.QPushButton(u'↑ 清空输入框') self.connect(self.btn_clearSendMessage, QtCore.SIGNAL( 'clicked()'), self.myClearSendMessage) layout.addWidget(self.btn_clearSendMessage, 5, 6, 1, 2) self.btn_quit = QtGui.QPushButton(u'退出') self.connect(self.btn_quit, QtCore.SIGNAL('clicked()'), self.myQuit) layout.addWidget(self.btn_quit, 5, 8, 1, 2) def myConnect(self): if self.isConnected == False: host = str(self.txt_ip.text()) port = int(self.txt_port.text()) try: self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) self.client_socket.connect((host, port)) except: self.txt_recvMessage.append(u'服务器连接失败,请检查网络连接或者稍后再试。') return thread.start_new_thread(self.recv_func, ()) # td = MyThread(self) # td.start() self.txt_recvMessage.append(u'服务器连接成功!') self.setWindowTitle(self.windowTitle() + ' --> ' + host + ':' + str(port)) self.isConnected = True self.btn_connect.setText(u'断开连接') else: self.disConnect() def disConnect(self): self.client_socket.close() self.txt_recvMessage.append(u'已断开与服务器的连接。') self.setWindowTitle(u'TCP客户端') self.isConnected = False self.btn_connect.setText(u'连接') def recv_func(self): while True: try: data = self.client_socket.recv(Client.BUF_LEN) except: break if not data or not len(data): break data = data[:-1] self.txt_recvMessage.append(data.decode('utf8')) # 很重要 self.disConnect() def myClearRecvMessage(self): self.txt_recvMessage.setText('') def myClearSendMessage(self): self.txt_sendMessage.setText('') def mySend(self): if self.isSendName.isChecked() == True: data = self.txt_name.text() if data == '': data = u'[匿名]' data = str((data + ': ' + self.txt_sendMessage.text() + '\n').toUtf8()) else: data = str((self.txt_sendMessage.text() + '\n').toUtf8()) try: self.client_socket.sendall(data) except: self.txt_recvMessage.append(u'消息发送失败...') return self.txt_sendMessage.setText('') def myQuit(self): self.close() def center(self): screen = QtGui.QDesktopWidget().screenGeometry() size = self.geometry() self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2) def closeEvent(self, event): reply = QtGui.QMessageBox.question(self, u'消息', u'你确定要退出吗?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: event.accept() try: self.client_socket.close() except: pass else: event.ignore() app = QtGui.QApplication(sys.argv) c = Client() c.show() sys.exit(app.exec_())
虽然有点小bug,不过主要功能已经能很好地实现了,以后有时间再来修改下。