自从开始学linux网络编程后就想写个聊天室,一开始原本打算用多进程的方式来写,可是发觉进程间的通信有点麻烦,而且开销也大,后来想用多线程能不能实现呢,于是便去看了一下linux里线程的用法,实际上只需要知道 pthread_create 就差不多了,于是动手开干,用了两天时间,调试的过程挺痛苦的,一开始打算用纯C来撸,便用简单的数组来存储客户端的连接信息,可是运行时出现了一些很奇怪的问题,不知道是不是访问了临界资源,和线程间的互斥有关等等;奇怪的是,当改用STL的set或map时问题就解决了,但上网搜了下发现STL也不是线程安全的,至于到底是什么问题暂时不想去纠结了,可能是其它一些小细节的错误吧。先贴上代码:

首先是必要的头文件 header.h:

  1. #ifndef __HEADER_H
  2. #define __HEADER_H
  3.  
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <string.h>
  7. #include <unistd.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <arpa/inet.h>
  12. #include <error.h>
  13. #include <signal.h>
  14. #include <sys/wait.h>
  15. #include <assert.h>
  16.  
  17. #include <pthread.h>
  18.  
  19. #define bool int // the 3 lines is for c originally
  20. #define true 1
  21. #define false 0
  22.  
  23. #define PORT 9003
  24. #define BUF_LEN 1024 // 缓冲区大小
  25. #define MAX_CONNECTION 6 // 服务器允许的最大连接数,可自行更改
  26.  
  27. #define For(i,s,t) for(i = (s); i != (t); ++i)
  28.  
  29. #endif // __HEADER_H

  然后是客户端部分 client.cpp,相对来说简单一些:

  1. #include "header.h"
  2.  
  3. // 客户端接收消息的线程函数
  4. void* recv_func(void *args)
  5. {
  6. char buf[BUF_LEN];
  7. int sock_fd = *(int*)args;
  8. while(true) {
  9. int n = recv(sock_fd, buf, BUF_LEN, );
  10. if(n <= ) break; // 这句很关键,一开始不知道可以用这个来判断通信是否结束,用了其它一些很奇葩的做法来结束并关闭 sock_fd 以避免 CLOSE_WAIT 和 FIN_WAIT2 状态的出现T.T
  11. write(STDOUT_FILENO, buf, n);
  12. }
  13. close(sock_fd);
  14. exit();
  15. }
  16.  
  17. // 客户端和服务端进行通信的处理函数
  18. void process(int sock_fd)
  19. {
  20. pthread_t td;
  21. pthread_create(&td, NULL, recv_func, (void*)&sock_fd); // 新开个线程来接收消息,避免了一读一写的原始模式,一开始竟把它放进 while 循环里面了,泪崩。。。
  22.  
  23. char buf[BUF_LEN];
  24. while(true) {
  25. int n = read(STDIN_FILENO, buf, BUF_LEN);
  26. buf[n++] = '\0'; // 貌似标准读入不会有字符串结束符的,需要自己手动添加
  27. send(sock_fd, buf, n, );
  28. }
  29. close(sock_fd);
  30. }
  31.  
  32. int main(int argc, char *argv[])
  33. {
  34. assert(argc == );
  35.  
  36. struct sockaddr_in cli;
  37. bzero(&cli, sizeof(cli));
  38. cli.sin_family = AF_INET;
  39. cli.sin_addr.s_addr = htonl(INADDR_ANY);
  40. cli.sin_port = htons(PORT); // 少了 htons 的话就连接不上了,因为小端机器的原因???
  41.  
  42. int sc = socket(AF_INET, SOCK_STREAM, );
  43. if(sc < ) {
  44. perror("socket error");
  45. exit(-);
  46. }
  47. inet_pton(AF_INET, argv[], &(cli.sin_addr)); // 用第一个参数作为连接服务器端的地址
  48.  
  49. int err = connect(sc, (struct sockaddr*)&cli, sizeof(cli));
  50. if(err < ) {
  51. perror("connect error");
  52. exit(-);
  53. }
  54. process(sc);
  55. close(sc);
  56.  
  57. return ;
  58. }

  最后是服务端 server.cpp:

  1. #include <map>
  2. #include "header.h"
  3. using std::map;
  4.  
  5. map<int, struct sockaddr_in*> socks; // 用于记录各个客户端,键是与客户端通信 socket 的文件描述符,值是对应的客户端的 sockaddr_in 的信息
  6.  
  7. // 群发消息给 socks 中的所有客户端
  8. inline void send_all(const char *buf, int len)
  9. {
  10. for(auto it = socks.begin(); it != socks.end(); ++it)
  11. send(it->first, buf, len, );
  12. }
  13.  
  14. // 服务端端接收消息的线程函数
  15. void* recv_func(void* args)
  16. {
  17. int cfd = *(int*)args;
  18. char buf[BUF_LEN];
  19. while(true) {
  20. int n = recv(cfd, buf, BUF_LEN, );
  21. if(n <= ) break; // 关键的一句,用于作为结束通信的判断
  22. write(STDOUT_FILENO, buf, n);
  23. if(strcmp(buf, "bye\n") == ) { // 如果接收到客户端的 bye,就结束通信并从 socks 中删除相应的文件描述符,动态申请的空间也应在删除前释放
  24. printf("close connection with client %d.\n", cfd);
  25. free(socks[cfd]);
  26. socks.erase(cfd);
  27. break;
  28. }
  29. send_all(buf, n); // 群发消息给所有已连接的客户端
  30. }
  31. close(cfd); // 关闭与这个客户端通信的文件描述符
  32. }
  33.  
  34. // 和某一个客户端通信的线程函数
  35. void* process(void *argv)
  36. {
  37. pthread_t td;
  38. pthread_create(&td, NULL, recv_func, (void*)argv); // 在主处理函数中再新开一个线程用于接收该客户端的消息
  39.  
  40. int sc = *(int*)argv;
  41. char buf[BUF_LEN];
  42. while(true) {
  43. int n = read(STDIN_FILENO, buf, BUF_LEN);
  44. buf[n++] = '\0'; // 和客户端一样需要自己手动添加字符串结束符
  45. send_all(buf, n); // 服务端自己的信息输入需要发给所有客户端
  46. }
  47. close(sc);
  48. }
  49.  
  50. int main(int argc, char *argv[])
  51. {
  52. struct sockaddr_in serv;
  53. bzero(&serv, sizeof(serv));
  54. serv.sin_family = AF_INET;
  55. serv.sin_addr.s_addr = htonl(INADDR_ANY);
  56. serv.sin_port = htons(PORT);
  57.  
  58. int ss = socket(AF_INET, SOCK_STREAM, );
  59. if(ss < ) {
  60. perror("socket error");
  61. return ;
  62. }
  63. int err = bind(ss, (struct sockaddr*)&serv, sizeof(serv));
  64. if(err < ) {
  65. perror("bind error");
  66. return ;
  67. }
  68. err = listen(ss, );
  69. if(err < ) {
  70. perror("listen error");
  71. return ;
  72. }
  73.  
  74. socks.clear(); // 清空 map
  75. socklen_t len = sizeof(struct sockaddr);
  76.  
  77. while(true) {
  78. struct sockaddr_in *cli_addr = (struct sockaddr_in*)malloc(sizeof(struct sockaddr_in));
  79. int sc = accept(ss, (struct sockaddr*)cli_addr, &len);
  80. if(sc < ) {
  81. free(cli_addr);
  82. continue;
  83. }
  84. if(socks.size() >= MAX_CONNECTION) { // 当将要超过最大连接数时,就让那个客户端先等一下
  85. char buf[] = "connections is too much, please waiting...\n";
  86. send(sc, buf, strlen(buf) + , );
  87. close(sc);
  88. free(cli_addr);
  89. continue;
  90. }
  91. socks[sc] = cli_addr; // 指向对应申请到的 sockaddr_in 空间
  92. printf("client %d connect me...\n", sc);
  93.  
  94. pthread_t td;
  95. pthread_create(&td, NULL, process, (void*)&sc); // 开一个线程来和 accept 的客户端进行交互
  96. }
  97. return ;
  98. }

  makefile文件:

  1. all: server client
  2. server: server.cpp
  3. g++ -std=c++11 -o server server.cpp -lpthread
  4. client: client.cpp
  5. g++ -std=c++11 -o client client.cpp -lpthread
  6. clean:
  7. rm -f *.o

  在我的ubuntu 14.04 64 位的机器上测试过没有什么问题,客户端与服务端能正常的交互和退出,能通过服务端接收其它客户端发送的消息,运行时cpu和内存占用情况正常,不会产生什么奇怪的bug。暂时只写了个终端的界面,客户端的UI迟点再去弄吧~

*****************************************************************************************************************************************

  今天试了下用 PyQt4 去写个客户端的界面,调了好一天,总算能看到点东西了,先上图:

  而命令行下的客户端(上面的 client.cpp 文件)的运行界面是这样子的:

  服务端的运行情况是:

  PyQt4 编写的客户端(pyqt_client.py)代码是:

  1. #!/usr/bin/env python
  2. #-*- coding: utf-8 -*-
  3.  
  4. from PyQt4 import QtGui, QtCore
  5. import sys
  6. import socket
  7. import thread
  8.  
  9. class Client(QtGui.QWidget):
  10.  
  11. BUF_LEN = 1024
  12.  
  13. def __init__(self, parent=None):
  14.  
  15. QtGui.QWidget.__init__(self, parent)
  16.  
  17. self.setWindowTitle(u'TCP客户端')
  18. self.resize(600, 500)
  19. self.center()
  20. layout = QtGui.QGridLayout(self)
  21.  
  22. label_ip = QtGui.QLabel(u'远程主机IP:')
  23. layout.addWidget(label_ip, 0, 0, 1, 1)
  24. self.txt_ip = QtGui.QLineEdit('127.0.0.1')
  25. layout.addWidget(self.txt_ip, 0, 1, 1, 3)
  26.  
  27. label_port = QtGui.QLabel(u'端口:')
  28. layout.addWidget(label_port, 0, 4, 1, 1)
  29. self.txt_port = QtGui.QLineEdit('')
  30. layout.addWidget(self.txt_port, 0, 5, 1, 3)
  31.  
  32. self.isConnected = False
  33. self.btn_connect = QtGui.QPushButton(u'连接')
  34. self.connect(self.btn_connect, QtCore.SIGNAL(
  35. 'clicked()'), self.myConnect)
  36. layout.addWidget(self.btn_connect, 0, 8, 1, 2)
  37.  
  38. label_recvMessage = QtGui.QLabel(u'消息内容:')
  39. layout.addWidget(label_recvMessage, 1, 0, 1, 1)
  40.  
  41. self.btn_clearRecvMessage = QtGui.QPushButton(u'↓ 清空消息框')
  42. self.connect(self.btn_clearRecvMessage, QtCore.SIGNAL(
  43. 'clicked()'), self.myClearRecvMessage)
  44. layout.addWidget(self.btn_clearRecvMessage, 1, 7, 1, 3)
  45.  
  46. self.txt_recvMessage = QtGui.QTextEdit()
  47. self.txt_recvMessage.setReadOnly(True)
  48. self.txt_recvMessage.setStyleSheet('background-color:yellow')
  49. layout.addWidget(self.txt_recvMessage, 2, 0, 1, 10)
  50.  
  51. lable_name = QtGui.QLabel(u'姓名(ID):')
  52. layout.addWidget(lable_name, 3, 0, 1, 1)
  53. self.txt_name = QtGui.QLineEdit()
  54. layout.addWidget(self.txt_name, 3, 1, 1, 3)
  55.  
  56. self.isSendName = QtGui.QRadioButton(u'发送姓名')
  57. self.isSendName.setChecked(False)
  58. layout.addWidget(self.isSendName, 3, 4, 1, 1)
  59.  
  60. label_sendMessage = QtGui.QLabel(u' 输入框:')
  61. layout.addWidget(label_sendMessage, 4, 0, 1, 1)
  62. self.txt_sendMessage = QtGui.QLineEdit()
  63. self.txt_sendMessage.setStyleSheet("background-color:cyan")
  64. layout.addWidget(self.txt_sendMessage, 4, 1, 1, 7)
  65. self.btn_send = QtGui.QPushButton(u'发送')
  66. self.connect(self.btn_send, QtCore.SIGNAL('clicked()'), self.mySend)
  67. layout.addWidget(self.btn_send, 4, 8, 1, 2)
  68.  
  69. self.btn_clearSendMessage = QtGui.QPushButton(u'↑ 清空输入框')
  70. self.connect(self.btn_clearSendMessage, QtCore.SIGNAL(
  71. 'clicked()'), self.myClearSendMessage)
  72. layout.addWidget(self.btn_clearSendMessage, 5, 6, 1, 2)
  73. self.btn_quit = QtGui.QPushButton(u'退出')
  74. self.connect(self.btn_quit, QtCore.SIGNAL('clicked()'), self.myQuit)
  75. layout.addWidget(self.btn_quit, 5, 8, 1, 2)
  76.  
  77. def myConnect(self):
  78. if self.isConnected == False:
  79. host = str(self.txt_ip.text())
  80. port = int(self.txt_port.text())
  81. try:
  82. self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
  83. self.client_socket.connect((host, port))
  84. except:
  85. self.txt_recvMessage.append(u'服务器连接失败,请检查网络连接或者稍后再试。')
  86. return
  87.  
  88. thread.start_new_thread(self.recv_func, ())
  89. # td = MyThread(self)
  90. # td.start()
  91. self.txt_recvMessage.append(u'服务器连接成功!')
  92. self.setWindowTitle(self.windowTitle() + ' --> ' + host + ':' + str(port))
  93. self.isConnected = True
  94. self.btn_connect.setText(u'断开连接')
  95. else:
  96. self.disConnect()
  97.  
  98. def disConnect(self):
  99. self.client_socket.close()
  100. self.txt_recvMessage.append(u'已断开与服务器的连接。')
  101. self.setWindowTitle(u'TCP客户端')
  102. self.isConnected = False
  103. self.btn_connect.setText(u'连接')
  104.  
  105. def recv_func(self):
  106. while True:
  107. try:
  108. data = self.client_socket.recv(Client.BUF_LEN)
  109. except:
  110. break
  111. if not data or not len(data):
  112. break
  113. data = data[:-1]
  114. self.txt_recvMessage.append(data.decode('utf8')) # 很重要
  115. self.disConnect()
  116.  
  117. def myClearRecvMessage(self):
  118. self.txt_recvMessage.setText('')
  119.  
  120. def myClearSendMessage(self):
  121. self.txt_sendMessage.setText('')
  122.  
  123. def mySend(self):
  124. if self.isSendName.isChecked() == True:
  125. data = self.txt_name.text()
  126. if data == '':
  127. data = u'[匿名]'
  128. data = str((data + ': ' + self.txt_sendMessage.text() + '\n').toUtf8())
  129. else:
  130. data = str((self.txt_sendMessage.text() + '\n').toUtf8())
  131. try:
  132. self.client_socket.sendall(data)
  133. except:
  134. self.txt_recvMessage.append(u'消息发送失败...')
  135. return
  136. self.txt_sendMessage.setText('')
  137.  
  138. def myQuit(self):
  139. self.close()
  140.  
  141. def center(self):
  142. screen = QtGui.QDesktopWidget().screenGeometry()
  143. size = self.geometry()
  144. self.move((screen.width() - size.width()) / 2,
  145. (screen.height() - size.height()) / 2)
  146.  
  147. def closeEvent(self, event):
  148. reply = QtGui.QMessageBox.question(self, u'消息', u'你确定要退出吗?',
  149. QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
  150. if reply == QtGui.QMessageBox.Yes:
  151. event.accept()
  152. try:
  153. self.client_socket.close()
  154. except:
  155. pass
  156. else:
  157. event.ignore()
  158.  
  159. app = QtGui.QApplication(sys.argv)
  160. c = Client()
  161. c.show()
  162. sys.exit(app.exec_())

  虽然有点小bug,不过主要功能已经能很好地实现了,以后有时间再来修改下。

  Github地址:https://github.com/NewdawnALM/TcpThreadChats

linux下使用多线程编写的聊天室的更多相关文章

  1. 【转】 Linux下的多线程编程

    作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/原文链接:http://www.cnblogs.com/gnuhpc/archive/2012/12/07/280 ...

  2. Linux下的多线程编程

    1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(proces ...

  3. 【转】Linux下的多线程编程

    1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(proces ...

  4. 《转》Linux下的多线程编程

    原地址:http://linux.chinaunix.net/doc/program/2001-08-11/642.shtml 1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程 ...

  5. linux下对qt编写的程序进行部署

    当我们完成程序设计之后,需要将可执行程序交付客户,而运行环境里面可能是没有相关支持库的,这个时候就涉及到部署的相关问题.对于我们在Linux下基于QT编写的图像处理程序,我们采用linuxdeploy ...

  6. Linux下模拟多线程的并发并发shell脚本

    分享一个在Linux下模拟多线程的并发脚本,使用这个脚本可以同时批量在定义数量的服务器上执行相关命令,比起普通for/while循环只能顺序一条一条执行的效率高非常多,在管理大批服务器时非常的实用.  ...

  7. Linux下shellcode的编写

    Linux下shellcode的编写 来源  https://xz.aliyun.com/t/2052 EdvisonV / 2018-02-14 22:00:42 / 浏览数 6638 技术文章 技 ...

  8. 【Java】Socket+多线程实现控制台聊天室

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/5827212.html 聊天室程序的结构图: 架构解释: Server服务器相当于一个中转站,Client客户端 ...

  9. Linux下基于多线程的echo

    准备开始写一些Linux 下网络编程以及多线程的blog,就从这个简单的echo程序开始吧. 在echo的服务端使用多线程与客户进行通信,可以实现一个服务端程序同时连接多个客户的功能.那么,到底在服务 ...

随机推荐

  1. jQuery瀑布流

  2. 与资源库同步时,我的svn报错 Previous operation has not finished; run 'cleanup' if it was interrupted

    解决办法:选择你的项目,右键,小组(Team),刷新或清理(Refresh or Clean)即可.

  3. Android Studio 使用Lambda

    1,昨天在使用RxJava的时候,调用map.filter之类的方法要创建挺多的匿名内部类,所以我们打算试用一下Lambda让我们的代码更有阅读新性,下看一下我们的对比 在使用之前我们代码是这样的 O ...

  4. 用node-inspector调试Node.js(转自NOANYLOVE'S BLOG)

    原文地址:http://www.noanylove.com/2011/12/node-the-inspector-debugging-node-js/ 用node-inspector调试Node.js ...

  5. 微信 回调模式 echostr校验失败,请您检查是否正确解密并输出明文echostr

  6. APICloud开发App总结(一)

    apiCloud app 开发是最近一两年刚刚兴起的一种混合开发方式.常用的模块以原生方式开发好,然后用js进行粘合.组织,完成整个的app的逻辑.这种开发方式极大的提高了软件模块的复用率,加快了ap ...

  7. tomcat8 配置在线管理应用功能

    在tomcat8下,更加注重安全性.如果要使用在管理控制台部署应用,需要修改更多的配置. 在$tomcat_base$/webapps/manager/META-INF/context.xml中 添加 ...

  8. 从svn资源库目录checkout出maven项目方法

    从svn资源库目录checkout出maven项目方法,如下图所示:

  9. ftp协议详解

    客户端与服务器之间,需要多条连接才能完成应用的协议,属于复杂协议.如FTP,PPTP,H.323和SIP均属于复杂协议. 这里主要介绍ftp协议的工作原理.首先,ftp通信协议有两种工作模式,被动模式 ...

  10. sax xpath读取xml字符串

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOExceptio ...