linux下使用多线程编写的聊天室

news/2024/10/3 18:47:48

  自从开始学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();          // 清空 mapsocklen_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.cppg++ -std=c++11 -o server server.cpp -lpthread
client: client.cppg++ -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 threadclass Client(QtGui.QWidget):BUF_LEN = 1024def __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 = Falseself.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'服务器连接失败,请检查网络连接或者稍后再试。')returnthread.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 = Trueself.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 = Falseself.btn_connect.setText(u'连接')def recv_func(self):while True:try:data = self.client_socket.recv(Client.BUF_LEN)except:breakif not data or not len(data):breakdata = 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:passelse:event.ignore()app = QtGui.QApplication(sys.argv)
c = Client()
c.show()
sys.exit(app.exec_())

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

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

转载于:https://www.cnblogs.com/Newdawn/p/5509687.html


https://dhexx.cn/news/show-591709.html

相关文章

SensorService开机启动耗时探讨

SensorService开机启动耗时探讨 android11-release SensorService启动 SensorService在单独线程启动为什么SystemServer耗时 在startOtherServices中WMS需要传感器服务准备就绪 SensorDevice 连接 Hal 底层 从SensorService启动可以看到主要可能是 SensorDevice connectH…

这些HTML、CSS知识点,面试和平时开发都需要 No5-No7

系列知识点汇总 这些HTML、CSS知识点&#xff0c;面试和平时开发都需要 No1-No4&#xff08;知识点&#xff1a;HTML、CSS、盒子模型、内容布局&#xff09; 这些HTML、CSS知识点&#xff0c;面试和平时开发都需要 No5-No7&#xff08;知识点&#xff1a;文字设置、设置背景、数…

installd守护进程

installd守护进程 android11-release PackageManagerServie 服务负责应用的安装、卸载等相关工作&#xff0c;而真正干活的还是 installd。 installd 启动 Android系统启动 查看 init 进程system/core/init/init.cpp中SecondStageMain&#xff0c;最终在 LoadBootScripts 解…

Java设计模式(十一) 享元模式

2019独角兽企业重金招聘Python工程师标准>>> 原创文章&#xff0c;同步发自作者个人博客&#xff0c;http://www.jasongj.com/design_pattern/flyweight/。转载请注明出处 享元模式介绍 享元模式适用场景 面向对象技术可以很好的解决一些灵活性或可扩展性问题&#…

JavaFx:添加顶部菜单 Microsoft Ribbon For JavaFX

JavaFx:添加顶部菜单 Microsoft Ribbon For JavaFX Microsoft Ribbon For JavaFX&#xff1a;Ribbon control for Java, implemented using JavaFX, based on the Microsoft Ribbon. Github FXRibbon FXRibbon-master 导入运行 运行ChangeAccentColorSample&#xff1a;更多内…

JavaFX:Robot高DPI截图

JavaFX:Robot高DPI截图 使用JDK中 java.awt.*&#xff1a;Robot、Rectangle JavaFX:截图功能 createScreenCapture 图片模糊 Iamge : (665.0,230.0) createMultiResolutionScreenCapture MultiResolutionImage mrImage robot.createMultiResolutionScreenCapture(rec); …

JavaFx:快捷键

JavaFx&#xff1a;快捷键 KeyCombination.html KeyCode.html 设置方式参考&#xff1a;JavaFX 设置快捷键、JavaFx&#xff1a;11、设置快捷键、JavaFX学习&#xff1a;快捷键 快捷键CTRL C KeyCombination ctrl_c new KeyCodeCombination(KeyCode.C, KeyCombination.CO…

Windows NTFS权限设置小结

在实际工作中经常会碰到NTFS文件夹权限设置的问题&#xff0c;比如&#xff1a;即使你赋予某用户full control并向子文件夹继承仍会出现access denied的情况&#xff0c;如下图&#xff1a;出现此情况的原因是由于赋权用户(如域管理员、本地管理员)没有某些子文件夹的full cont…

JavaFX: 多语言适配

JavaFX: 多语言适配 JDK国际化&#xff1a;ResourceBundle.html 其他资源&#xff1a;TornadoFX编程指南&#xff0c;第10章&#xff0c;FXML和国际化、JavaFX的ResourceBundle使用 创建Resource Bundle资源 ResourceBundle获取资源 public class ResourceBundleUtil {priva…

一组Linux Shell Scripting小练习

原创作品&#xff0c;允许转载&#xff0c;转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://dgd2010.blog.51cto.com/1539422/1718284 # Linux shell将字符串分割成数组 12result$(facter | awk /ipaddress/ && !/ipaddres…