具体功能

  1. 服务器通过TCP与客户端通信且可以与串口终端设备通信,端口号和串口号波特率采用带参执行

  2. 服务端可以支持多个客户端和多个串口

  3. makefile文件编写

  4. 如果连接客户端过多,则随机踢掉一个

  5. 通过简易私有协议实现串口和服务器之间的双向通信

代码汇总:所有代码见附录!

0 文档结构

src/:用于存储源文件

  • makefile:用于编译.c文件,并生成合适的动态库

inc/:用于存储头文件

lib/:用于存储动态库文件

tools/:一些shell脚本工具文件

makefile:用于编译main函数,生成最终的out可执行程序

readme.md:详细的实现方案

如上是我文档的规划方法,可以参考,如果不采用,那下面代码的路径需要进行更改。

总体流程框图

功能一:

1.1带参执行

参考实例如下:

int main(int argc, char **argv)  #其中argc表示参数的个数,argv为一个数组,里面通过字符串的方式保存了各项参数
{// 判断传入的参数是否为4个if(argc != 4){printf("./main tcp_port usar_port bps \n");exit(1);}//注意这里的参数为字符串类型,需要转换成整型int SERVER_PORT= atoi(argv[1]);int USART_PORT= argv[2];int USART_BPS= atoi(argv[3]);}

通过带参执行可以在服务器运行时,人为简单的设置tcp端口,串口端口,串口波特率。

1.2服务器与客户端通过TCP通信


关于socket编程,点击下面连接有详细讲解

  • socket()套接字函数
  • bind()绑定函数
  • listen()监听函数
  • accept()接收函数
  • connect()连接函数
  • Recv()接收函数函数
  • Send()发送函数

1.3 服务器与串口通信

串口通信

  • open():打开串口,建议写成bool函数,判断是否打开成功
  • 串口初始化:设置波特率,数据位,奇偶校验位,停止位
  • read/write():读写函数
  • close:关闭串口

前面的socket 包括这里的usart 如果采用面向对象编程将获得很好的效果

打开串口

/**************************************************************** @name OpenSerialPort()* @function 打开串口设备* @para     NULL* @return 打开成功返回0,失败则返回-1* *************************************************************/
int OpenSerialPort()
{serial_fd = open(serial_port, O_RDWR|O_NOCTTY|O_NDELAY);  //以读写、不允许进程管理串口、非阻塞打开串口(若打开成功返回文件描述符,否则返回-1)if(serial_fd == -1){cout<<"open "<<serial_port<<" failed"<<endl;return -1;}else{cout<<"open "<<serial_port<<" success"<<endl;}/*设置串口设备文件为阻塞状态*/if(fcntl(serial_fd,F_SETFL,0) < 0){cout<<"recover "<<serial_port<<" to block state failed"<<endl;}else{cout<<serial_port<<"is block state"<<endl;}/*测试是否为终端设备*/if (isatty(STDIN_FILENO) == 0){cout<<"standard input is not a terminal device"<<endl;}else{cout<<"is a terminal device!"<<endl;}return 0;
}

串口初始化

/******************************************************************* @name InitSerialPort()* @function 串口初始化,设置波特率、数据位、奇偶校验位、停止位* @para nBaud:串口波特率(4800、9600、115200)*          nBits:数据位(5、6、7、8)*         nStop:停止位(1、2)*       Parity:奇偶校验位('O','E','N')* @return 初始化成功则返回0,失败则返回-1* ****************************************************************/
int InitSerialPort(int nBaud, int nBits, int nStop, char Parity)
{struct termios usart_config;/*先获取串口的原有配置*/if(tcgetattr(serial_fd, &usart_config) != 0){cout<<"get usart_config failed"<<endl;return -1;}memset(&usart_config, 0, sizeof(usart_config)); //清零/*设置波特率*/usart_config.c_cflag |= (CLOCAL|CREAD);       //设置本地模式、使能接收switch(nBaud){case 4800:cfsetispeed(&usart_config, B4800); //设置输入速率cfsetospeed(&usart_config, B4800);  //设置输出速率break;case 9600:cfsetispeed(&usart_config, B9600);  //设置输入速率cfsetospeed(&usart_config, B9600);  //设置输出速率break;case 115200:cfsetispeed(&usart_config, B115200);  //设置输入速率cfsetospeed(&usart_config, B115200);    //设置输出速率break;default:cfsetispeed(&usart_config, B9600);    //未设置则默认为9600cfsetospeed(&usart_config, B9600); //未设置则默认为9600}/*设置数据位*/usart_config.c_cflag &= ~CSIZE;     //清零c_cflag中数据位数大小switch(nBits){case 5:usart_config.c_cflag |= CS5;    //设置数据位为5位break;case 6:usart_config.c_cflag |= CS6;    //设置数据位为6位break;case 7:usart_config.c_cflag |= CS7;    //设置数据位为7位break;case 8:usart_config.c_cflag |= CS8;    //设置数据位为8位break;default:usart_config.c_cflag |= CS8;   //未设置则默认为8位}/*设置停止位*/if (nStop == 1){usart_config.c_cflag &= ~CSTOPB;    //设置1位停止位}else if (nStop == 2){usart_config.c_cflag |= CSTOPB;       //设置2位停止位}else{usart_config.c_cflag &= ~CSTOPB;    //若为其他数字则默认设置为1位停止位}/*设置奇偶校验位*/switch(Parity){case 'N':usart_config.c_cflag &= ~PARENB;  //禁止奇偶校验位(不使能奇偶校验位)break;case 'O':usart_config.c_cflag |= PARENB;      //使能校验位usart_config.c_cflag |= PARODD;     //设置奇校验usart_config.c_iflag |= (INPCK | ISTRIP);break;case 'E':usart_config.c_cflag |= PARENB;      //使能校验位usart_config.c_cflag |= ~PARODD;    //设置偶校验位usart_config.c_iflag |= (INPCK | ISTRIP);break;default:usart_config.c_cflag &= ~PARENB;   //禁止奇偶校验位(不使能奇偶校验位)}usart_config.c_cc[VTIME] = 0;    //VTIME:设置读取字符的usart_config.c_cc[VMIN] = 0; //指定要读取的最小字符数tcflush(serial_fd, TCIOFLUSH); //清空输入输出缓存if(tcsetattr(serial_fd, TCSANOW, &usart_config) != 0){cout<<"usart set failed"<<endl;return -1;}cout<<"usart set success:"<<endl;return 0;
}

串口读写

/********************************************************************** @name SerialPortReadData()* @function 读取串口的数据* @para *readBuff:读取数组首地址*         dataLen:读取多少个字节* @return 返回读取的字节数目* @note 注意在使用时最好加上延时,对于目前此种读法好像会立即返回* ******************************************************************/
int SerialPortReadData(char *readBuff, size_t dataLen)
{int character_num=0;character_num = read(serial_fd, readBuff, dataLen);  //读取数据return character_num;
}/********************************************************************** @name SerialPortWriteData()* @function 向串口写数据* @para *writeBuff:写入数组首地址*          dataLen:写入多少个多少个字节* @return 返回读取的字节数目* ******************************************************************/
size_t SerialPortWriteData(char *writeBuff, size_t dataLen)
{int character_num=0;character_num = write(serial_fd, writeBuff, dataLen);    //写入数据return character_num;
}

关闭串口

/******************************************************************** @name SerialPortClose()* @fuction 关闭串口* *****************************************************************/
int SerialPortClose()
{return close(serial_fd);
}

只需要将客户端发送的信息,原样转发给串口,反之一样即可

1.4 透明传输

即发送端的数据可以无差错的传送到接收端。

  • 出错原因:数据里的终止符被误识别 ; 传送过程中噪声的误码
  • 解决办法:在终止符前面加上转义字符 ; 采用奇偶校验,CRC,海明码等方式进行误码判断

这里串口采用奇偶校验的方式进行判断是否传输错误。

1.5 通信配置问题

本次项目是通过VMWare下虚拟机实现,版本是Ubuntu20.04,为了实现虚拟机远程通信,我们需要了解如下内容。

VMWare提供三种工作模式,桥接模式,NAT模式,host-only主机模式

  1. bridged(桥接模式)

在这种模式下,VMWare虚拟出来的操作系统就像是局域网中的一台独立的主机,它可以访问网内任何一台机器。在桥接模式下,你需要手工为虚拟 系统配置IP地址、子网掩码,而且还要和宿主机器处于同一网段,这样虚拟系统才能和宿主机器进行通信。同时,由于这个虚拟系统是局域网中的一个独立的主机系统,那么就可以手工配置它的TCP/IP配置信息,以实现通过局域网的网关或路由器访问互联网。

使用桥接模式的虚拟系统和宿主机器的关系,就像连接在同一个Hub上的两台电脑。想让它们相互通讯,你就需要为虚拟系统配置IP地址和子网掩码,否则就无法通信。

如果你想利用VMWare在局域网内新建一个虚拟服务器,为局域网用户提供网络服务,就应该选择桥接模式。

  1. host-only(主机模式)

在某些特殊的网络调试环境中,要求将真实环境和虚拟环境隔离开,这时你就可采用host-only模式。在host-only模式中,所有的虚拟系统是可以相互通信的,但虚拟系统和真实的网络是被隔离开的。

提示:在host-only模式下,虚拟系统和宿主机器系统是可以相互通信的,相当于这两台机器通过双绞线互连。

在host-only模式下,虚拟系统的TCP/IP配置信息(如IP地址、网关地址、DNS服务器等),都是由VMnet1(host-only)虚拟网络的DHCP服务器来动态分配的。

如果你想利用VMWare创建一个与网内其他机器相隔离的虚拟系统,进行某些特殊的网络调试工作,可以选择host-only模式。

  1. NAT(网络地址转换模式)

使用NAT模式,就是让虚拟系统借助 NAT(网络地址转换)功能,通过宿主机器所在的网络来访问公网。也就是说,使用NAT模式可以实现在虚拟系统里访问互联网。NAT模式下的虚拟系统的TCP/IP配置信息是由VMnet8(NAT)虚拟网络的DHCP服务器提供的,无法进行手工修改,因此虚 拟系统也就无法和本局域网中的其他真实主机进行通讯。采用NAT模式最大的优势是虚拟系统接入互联网非常简单,你不需要进行任何其他的配置,只需要宿主机 器能访问互联网即可。

由上面三种模式可以得出,这里采用桥接模式才能实现局域网中虚拟机和远程主机进行通信

桥接模式

为了实现远程通信,我们就需要虚拟出来的操作系统就像局域网中的一台独立的主机一样,因此采用桥接模式。

因为当时切换桥接的时候,自动分配好了地址,掩码等,所以如果桥接模式没有网络的话,建议手动配置一下

在同一个wifi下,即可进行通信,配置项如下:

  • ipv4的地址
  • 子网掩码
  • 网关

tcp网络通信配置

简单的测试本地通信测试:

主机地址:127.0.0.1 端口号:7070

linux端为服务器

主机地址:ifconfig查询 端口号:netstat查询

windows端为服务器

主机地址:ipconfig查询 端口号:netstat查询,寻找TIME_WAIT端口

串口通信

串口通信

  • dmesg |grep tty 查询串口端口
  • ls -l ttyS* 显示串口
  • lsmod 显示驱动
  • cat /proc/tty/driver/serial 查询真实串口

虚拟串口通信

  • 下载virtual serial port,然后配置好虚拟串口
  • 重启电脑,检查串口数量,且在虚拟机关机前配置好虚拟机
  • 注意上述重启电脑很关键

真实串口通信

  • 安装驱动
  • 点开串口助手
  • 设置好波特率,数据位,停止位等保持一致

功能二

2.1 服务端与多个客户端

从最开始服务器和客户端的流程图进行分析,这里需要用到多线程,如当我们将accept接收函数通过子线程挂起,将每一个客户端和服务器都单独开一个线程,就能够实现服务端和多个客户端同时连接。具体实现如下所示:

子线程接受客户端代码

/********************************************************************
*   Function Name: void *fun_thrAcceptHandler(void *socketListen)
*   Description: 监听客户端的连接请求,获取待连接客户端的网络信息,并为该客户端创建子线程.
*   Called By: server.c[main]
*   Input: socketListen -> 表示用于监听的被动套接字
*   Date: 2021/09/13
*********************************************************************/
void *fun_thrAcceptHandler(void *socketListen){   //参数为套接字while(1){int sockaddr_in_size = sizeof(struct sockaddr_in); //获得套接字的长度struct sockaddr_in client_addr;   //创建客户端地址int _socketListen = *((int *)socketListen);/* 接收相应的客户端的连接请求 */int socketCon = accept(_socketListen, (struct sockaddr *)(&client_addr), (socklen_t *)(&sockaddr_in_size));if(socketCon < 0){printf("call accept()");}else{printf("Connected %s:%d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);}printf("Client socket: %d\n", socketCon);/* 获取新客户端的网络信息 */_MySocketInfo socketInfo;socketInfo.socketCon = socketCon;socketInfo.ipaddr = inet_ntoa(client_addr.sin_addr);socketInfo.port = client_addr.sin_port;/* 将新客户端的网络信息保存在 arrConSocket 数组中 */arrConSocket[conClientCount] = socketInfo;conClientCount++;printf("Number of users: %d\n", conClientCount);/* 为新连接的客户端开辟线程 fun_thrReceiveHandler,该线程用来循环接收客户端的数据 */pthread_t thrReceive = 0;pthread_create(&thrReceive, NULL, fun_thrReceiveHandler, &socketInfo);arrThrReceiveClient[thrReceiveClientCount] = thrReceive;thrReceiveClientCount ++;printf("A thread has been created for the user.\n");/* 让进程休息0.1秒 */usleep(100000);}char *s = "Safe exit from the receive process ...";pthread_exit(s);
}

上述代码完成两件事情

  • 监听客户端请求,并创建对接客户端的线程挂起。
  • 管理客户端套接字与线程

服务器与客户端通信的子线程

/********************************************************************
*   Function Name: void *fun_thrReceiveHandler(void *socketInfo)
*   Description: 向服务器发送初始消息,从服务器循环接收信息.
*   Called By: server.c[main]
*   Input: socketInfo -> 表示客户端的网络信息
*   Date: 2021/09/13
*********************************************************************/
void *fun_thrReceiveHandler(void *socketInfo){   // 客户端的信息int buffer_length;int con;int i;_MySocketInfo _socketInfo = *((_MySocketInfo *)socketInfo); //强转,备份一份信息/* 向服务器发送握手消息 */send(_socketInfo.socketCon, HANDSHARK_MSG, sizeof(HANDSHARK_MSG), 0);/* 从服务器循环接收消息 */while(1){// 将接收缓冲区buffer清空bzero(&buffer,sizeof(buffer));// 接收服务器信息printf("Receiving messages from client %d ...\n", _socketInfo.socketCon);buffer_length = recv(_socketInfo.socketCon, buffer, BUFSIZ, 0);if(buffer_length == 0){// 判断为客户端退出printf("%s:%d Closed!\n", _socketInfo.ipaddr, _socketInfo.port);// 找到该客户端在数组中的位置for(con = 0; con < conClientCount; con++){if(arrConSocket[con].socketCon == _socketInfo.socketCon){break;}}// 将该客户端的信息删除,重置客户端数组for(i = con; i < conClientCount-1; i++){arrConSocket[i] = arrConSocket[i+1];}conClientCount --;break;}else if(buffer_length < 0){printf("Fail to call read()\n");break;}buffer[buffer_length] = '\0';printf("%s:%d said:%s\n", _socketInfo.ipaddr, _socketInfo.port, buffer);ReadToSend = 1;     // 发送标志置位,允许主线程发送数据usleep(100000);}printf("%s:%d Exit\n", _socketInfo.ipaddr, _socketInfo.port);return NULL;
}

多线程结构

子线程fun_thrAcceptHandler:单开accept()持续接受客户端,并为其开辟一道子线程,同时计数。

子线程fun_thrReceiveHandler:接受客户端的信息,并设置主线程标志位

主线程:得到子线程的标志位之后,广播主线程的信息返还给所有主机

功能三

src中的makefile:

CC=gcc   #默认为C
CFLAGS=
CFILES=$(wildcard *.c)  #找出所有的.c文件,并用空格隔开
OBJS=$(CFILES)
REAL_NAME=libmysocket.so.1.0  #动态库真实的名字
SO_NAME=libmysocket.so.1      #小版本改动不影响,只有大版本改动才会对其进行更改
LINK_NAME=libmysocket.so      #可有可无,在gcc时使用,避免版本变化导致其不可用
LIB_DIR=/usr/lib
INC_DIR=/home/fsj/mytasks/bigtask/src
SAVE_DIR=/home/fsj/mytasks/bigtask/liblibyear.so.1.0:# 此为生成动态库文件# 将soname文件连接到真实动态库之中$(CC) -fpic -shared -Wl,-soname,$(SO_NAME) -o $(REAL_NAME) $(OBJS) -I$(SAVE_DIR) -lpthreadinstall:sudo rm $(SAVE_DIR)/$(LINK_NAME) #删除lib库的文件ln -s $(REAL_NAME) $(LINK_NAME)   #用于编译sudo rm $(LIB_DIR)/$(SO_NAME)    #删除原有的大版本sudo ln -s $(REAL_NAME) $(LIB_DIR)/$(SO_NAME)   #复制真实版本的动态连接库sudo cp $(REAL_NAME) $(LIB_DIR)  #复制到usr/lib文件下sudo mv $(REAL_NAME) $(SAVE_DIR) #将生成的动态库放入到lib文件中
clean:rm *.o *.so *.so*.*

作用:生成动态链接库。分别为SONAME,REALNAME,LINK_NAME

install这里的第一句话,删除lib库的文件,在第一次编译的时候,请注释掉,因为第一次编译还没有该文件。

使用时,注意LIB_DIR,INC_DIR,SAVE_DIR的路径按照需要更改

这里建议重新手打一遍,makefile编写格式要求严格,这里的代码注释是之后才加上去的。

总程序的makefile:

CC=gcc
CFLAGS=
CFILES=$(wildcard *.c)
OBJS=$(CFILES:%.c=%.o)  #将变量的.c替换成.o:
LIB_DIR=/usr/lib
INC_DIR=/home/fsj/mytasks/bigtask/src
SAVE_DIR=/home/fsj/mytasks/bigtask/lib
all:main
main:$(OBJS)# -L 目录作为第一个寻找库文件(linker-name)的目录-L$(INC_DIR) # -lworld 表示在-L$(INC_DIR)下寻找 libworld.so 动态库文件(linker-name) # gcc 加入链接参数“-Wl,-rpath”指定动态库(so-name,real-name)运行时搜索路径$(CC) -Wl,-rpath,$(LIB_DIR) $(OBJS) -o main.out -L$(SAVE_DIR) -lmysocket -lpthread
.c.o: #将所有的.c文件编译成.o文件$(CC) -c $< -I$(INC_DIR)
clean:rm -rf main.out *.o *~ *.rar

作用:生成最后的可执行文件

功能四:

4.1 当客户端达到一定数量后,能随机踢掉一个

两步走:动态统计连接客户端的数量 ; 踢掉客户端的原则

动态统计客户端数量

int checkThrIsKill(pthread_t thr){int res = 1;int res_kill = pthread_kill(thr, 0);if(res_kill == 0){res = 0;}return res;
}

计数流程

  • fun_thrAcceptHandler:每连接一个客户端,conClientCount+1 ;每创建一个子线程, thrReceiveClientCount+1
  • fun_thrReceiveHandler:判断客户端是否死掉,死掉conClientCount-1
  • 主线程:判断线程是否死掉(即客户端死掉后,数组是否进行了更新),thrReceiveClientCount-1

动态统计客户端除了记录连接的客户端,还要时刻关注断开连接的客户端,并杀死子线程

这里分别定义了线程数组和客户端数组

剔除客户端原则

当客户端的时候超过10个时,将连接最早的客户端剔除

  • 当客户端的数量超过4个时,我们杀死线程数组最前面的数组,然后重置两个数组即可
  • pthread_cancel():向线程发送取消信号
  • pthread_setcancelstat():当该线程收到取消信号之后,选择是否忽略取消信号
  • pthread_testcancel():人为设置取消点

因为创建客户端线程的程序在accept()里面,所以我们杀死进程的程序放入到accept()的子线程,这样可以在创建客户端的时候及时剔除前面的客户端。

功能五

5.1 通过简易私有协议实现串口终端与客户端双向通信

可以在TCP层之上先进行一层私有协议封装

起始标识(1字节) 客户端地址(8字节) 串口号地址(1字节) 终止字符(1字节)
$ 端口号作为地址 0:串口0 , 1:串口1 ,2:串口12 @

起始符号:判断是否客户端或串口发送的信息。

客户端地址:采用16进制,2字节表示一段,一共4段,即16进制ASCII地址转换为10进制ASCII地址,如C0:A8:01:89 表示为 192.168.1.137。

串口地址:本次试验一共两个串口,分别用0,1来表示。

协同传输:用来判断信息的发送方式是串行,还是并行,这里需要检测连接的串口数量。

终止字符:判断消息的结束。

FFFFFFFF:广播 ; 2 多个串口传输

TCP助手和串口助手

调试助手见下面连接,如果掉了,提醒我补充
链接:调试助手链接
提取码:ryib

总结

就自己感觉而言,还算比较经典的服务器实现。但是由于当时初出茅庐,啥也不懂,所以很多东西没有实现,很遗憾。

  • 采用select\poll\epoll等高并发的函数,特别是现在流程的epoll,实现多客户端连接。

  • 没有实现对于客户端非正常断开,进行判断,可以添加心跳包程序,进行客户端与服务器连接的实时判断。

  • 对于客户端和线程的管理采用的数据结构效率较低

但是也算是一次很不错的经历,收获满满!

老规矩,如果有帮助,希望点赞,关注加收藏,如果有疑问,欢迎随时沟通。

附录

代码结构

  • mysocket.c:socket编程封装,对于几个线程函数进行了封装。
  • mysocket.h:mysocket.c的头文件
  • usart.c:串口编程的封装,包括打开串口,初始化串口,读写
  • usart.h:usart.c的头文件
  • main.c:服务器的主要实现,(客户端------服务器-------串口终端设备)

main.c

/********************************************************************
*   File Name: server.c
*   Description: 用于实现多客户端通信
*   Others:
*     1. 服务器首先创建套接字并绑定网络信息
*     2. 之后创建一个子线程用于接收客户端的网络请求
*     3. 在子线程中接收客户端的网络请求,并为每一个客户端创建新的子线程,该子线程用于服务器接收客户端数据
*     4. 服务器的主线程用于向所有客户端循环发送数据
*   Init Date: 2021/09/13
*********************************************************************/
#include "./inc/mysocket.h"
#include "./inc/usart.h"char usart_port = '0';    //usart_port
char client_ipaddr[20];long hexToDec(char *source);
int getIndexOfSigns(char ch);
int checkTopMessage(char* buff);int main(int argc, char **argv)
{ReadToSend = 0;conClientCount = 0;thrReceiveClientCount = 0;struct argument arg1,arg2;if(argc != 5){printf("./main tcp_port usar_port bps \n");exit(1);}int SERVER_PORT= atoi(argv[1]);   // tcp端口arg1.serial_port = argv[2];arg2.serial_port = argv[3];arg1.USART_BPS= atoi(argv[4]);     // 波特率arg2.USART_BPS= atoi(argv[4]);     // 波特率printf("serial_port,%s\n",arg1.serial_port);//serial_port = "/dev/ttyUSB0";//int USART_BPS = 9600;/**********************************创建服务器*********************************/printf("开始打开服务器...\n");/* 创建TCP连接的Socket套接字 */int socketListen = socket(AF_INET, SOCK_STREAM, 0);if(socketListen < 0){printf("没能创建socket套接字\n");exit(-1);}else{printf("成功创建socket套接字.\n");}/* 填充服务器端口地址信息,以便下面使用此地址和端口监听 */struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;// 这里的地址使用所有本地网络设备的地址,表示服务器会接收任意地址的客户端信息server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(SERVER_PORT);/* 将套接字绑定到服务器的网络地址上 */if(bind(socketListen, (struct sockaddr *)&server_addr,sizeof(struct sockaddr)) != 0){perror("绑定错误");exit(-1);}printf("套接字绑定服务器地址成功\n");/* 开始监听相应的端口 */if(listen(socketListen, 10) != 0){perror("监听失败");exit(-1);}printf("打开监听成功\n");/**********************************串口设备*********************************/pthread_t uasrt;printf("创建2个串口子线程\n");pthread_create(&uasrt,NULL,(void *) usart_handler,&arg1);pthread_t uasrt1;pthread_create(&uasrt1,NULL,(void *)usart_handler,&arg2);/**********************************客户端设备*********************************//* 创建一个线程用来接收客户端的连接请求 */pthread_t thrAccept;printf("创建一个接受客户端子线程\n");pthread_create(&thrAccept, NULL, fun_thrAcceptHandler, &socketListen);/**********************************主程序*********************************/int i = 0;int send_usart_len = 0;/* 主线程用来向所有客户端循环发送数据 */while(1){if (UarstToSend){int res = checkTopMessage(buf);if(res < 0){printf("message is error\n");UarstToSend = 0;} else{//寻找对应的客户端for(i = 0; i < conClientCount; i++){if(strcmp(arrConSocket[i].ipaddr,client_ipaddr)==0||res == 2){printf("socketCon = %d\n buffer is: %s\n", arrConSocket[i].socketCon, buf);int sendMsg_len = send(arrConSocket[i].socketCon, buf, strlen(buf), 0);if(sendMsg_len > 0){printf("Send Message to %s:%d successful\n", arrConSocket[i].ipaddr, arrConSocket[i].port);memset(buf, 0, 100);}else{printf("Fail to send message to %s:%d\n", arrConSocket[i].ipaddr, arrConSocket[i].port);}UarstToSend = 0;}}}if(UarstToSend != 0);{printf("fail to find the cliend:%s\n",client_ipaddr);UarstToSend = 0;}printf("#############################\n");}if(ReadToSend){// 判断线程存活数量int i;int j;for(i = 0; i < thrReceiveClientCount; i++){if(checkThrIsKill(arrThrReceiveClient[i])){printf("A Thread has been killed\n");// 重置线程数组for(j = i; j < thrReceiveClientCount-1; j++){arrThrReceiveClient[j] = arrThrReceiveClient[j+1];} thrReceiveClientCount --;}}printf("Number of connected client: %d\n", thrReceiveClientCount);if(conClientCount <= 0){printf("No Clients!\n");}// 给相应的串口发送消息else{printf("conClientCount = %d\n", conClientCount);//核查信息串口的地址int res = checkTopMessage(buffer);if(res < 0){printf("message is error\n");ReadToSend = 0;}   else{// 转接消息给串口设备if(usart_port=='0'){send_usart_len = SerialPortWriteData(&arg1.serial_fd,buffer, strlen(buffer));}else if(usart_port=='1'){send_usart_len = SerialPortWriteData(&arg2.serial_fd,buffer, strlen(buffer));}else if(usart_port=='2'){send_usart_len = SerialPortWriteData(&arg1.serial_fd,buffer, strlen(buffer));send_usart_len = SerialPortWriteData(&arg2.serial_fd,buffer, strlen(buffer));}if(send_usart_len>0){printf("send message to usart is successful \n");ReadToSend = 0;}else{printf("Fail to send message to usart \n");ReadToSend = 0;}}            }}sleep(0.5);}/* 等待子进程退出 */printf("Waiting for child thread to exit ....\n");char *message;pthread_join(thrAccept,(void *)&message);printf("%s\n",message);return 0;
}/********************************************************************
*   Function Name: checkTopMessage(pthread_t thr)
*   Description: 检查客户端发送的信息目的
*   Called By: server.c[main]
*   Input: 无需输入
*   Date: 2021/09/13
*********************************************************************/
int checkTopMessage(char* buff){char msg[20];int len =11;int i = 0;int addr_int = 0;char addr_char[5];strncpy(msg,buff,len);msg[len] = '\0';printf("msg : %s\n", msg);//解析里面字段,1~8为客户端地址,9为串口地址if(msg[0]!='$') return -1;if(msg[10]!='@') return -1;memset(client_ipaddr,'\0',sizeof(*client_ipaddr));  //清零for(i=1; i<8;i+=2){addr_int = getIndexOfSigns(msg[i])*16+getIndexOfSigns(msg[i+1]);sprintf(addr_char,"%d",addr_int);strcat(client_ipaddr,addr_char);if(i!=7) strcat(client_ipaddr,".");}if (strcmp(client_ipaddr,"255.255.255.255")==0){printf("this is a all message\n");return 2; }usart_port = msg[9];   // usart portprintf("usart_port:%c\t",usart_port);printf("client_ipaddr:%s\n",client_ipaddr);return 1;
}/********************************************************************
*   Function Name: hexToDec(char *source)
*   Description: 十六进制转换为十进制
*   Called By: server.c[main]
*   Input: 字符串
*   Date: 2021/09/13
*********************************************************************/long hexToDec(char *source)
{long sum = 0;long t = 1;int i,len;len = strlen(source);for(i=len-1; i>0; i--){sum+=t*getIndexOfSigns(*(source+i));t *= 16;}return sum;
}/********************************************************************
*   Function Name: getIndexOfSigns(char source)
*   Description: 返回ch字符在sign数组中的序号
*   Called By: server.c[main]
*   Input: 字符串
*   Date: 2021/09/13
*********************************************************************/int getIndexOfSigns(char ch)
{if(ch >= '0' && ch <= '9')  return ch-'0';if(ch >= 'A' && ch <= 'F')  return ch-'A'+10;if(ch >= 'a' && ch <= 'f')  return ch-'a'+10;return -1;
}

mysocket.c

#include"../inc/mysocket.h"/********************************************************************
*   Function Name: void *fun_thrAcceptHandler(void *socketListen)
*   Description: 监听客户端的连接请求,获取待连接客户端的网络信息,并为该客户端创建子线程.
*   Called By: server.c[main]
*   Input: socketListen -> 表示用于监听的被动套接字
*   Date: 2021/09/13
*********************************************************************/
void *fun_thrAcceptHandler(void *socketListen){int i = 0;while(1){int sockaddr_in_size = sizeof(struct sockaddr_in);struct sockaddr_in client_addr;int _socketListen = *((int *)socketListen);/* 接收相应的客户端的连接请求 */int socketCon = accept(_socketListen, (struct sockaddr *)(&client_addr), (socklen_t *)(&sockaddr_in_size));if(socketCon < 0){printf("call accept()");}else{printf("Connected %s:%d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);}printf("Client socket: %d\n", socketCon);if(conClientCount >= MaxClientNum){// 判断为客户端退出printf("%s:%d Closed!\n", arrConSocket[0].ipaddr, arrConSocket[0].port);// 数组重排pthread_cancel(arrThrReceiveClient[0]);pthread_join(arrThrReceiveClient[0],NULL);// 将该客户端的信息删除,重置客户端数组for(i = 0; i < conClientCount-1; i++){arrConSocket[i] = arrConSocket[i+1];}conClientCount --;// 重置线程数组for(i = 0; i < thrReceiveClientCount-1; i++){arrThrReceiveClient[i] = arrThrReceiveClient[i+1];}thrReceiveClientCount --;}/* 获取新客户端的网络信息 */_MySocketInfo socketInfo;socketInfo.socketCon = socketCon;socketInfo.ipaddr = inet_ntoa(client_addr.sin_addr);socketInfo.port = client_addr.sin_port;/* 将新客户端的网络信息保存在 arrConSocket 数组中 */arrConSocket[conClientCount] = socketInfo;conClientCount++;printf("客户端数量: %d\n", conClientCount);/* 为新连接的客户端开辟线程 fun_thrReceiveHandler,该线程用来循环接收客户端的数据 */pthread_t thrReceive = 0;pthread_create(&thrReceive, NULL, fun_thrReceiveHandler, &socketInfo);arrThrReceiveClient[thrReceiveClientCount] = thrReceive;thrReceiveClientCount ++;printf("创建了一个客户端线程 \n");/* 让进程休息0.1秒 */usleep(100000);}char *s = "Safe exit from the receive process ...";pthread_exit(s);
}/********************************************************************
*   Function Name: void *fun_thrReceiveHandler(void *socketInfo)
*   Description: 向服务器发送初始消息,从服务器循环接收信息.
*   Called By: server.c[main]
*   Input: socketInfo -> 表示客户端的网络信息
*   Date: 2021/09/13
*********************************************************************/
void *fun_thrReceiveHandler(void *socketInfo){int buffer_length;int con;int i;char msg[20];_MySocketInfo _socketInfo = *((_MySocketInfo *)socketInfo);/* 向服务器发送握手消息 */send(_socketInfo.socketCon, HANDSHARK_MSG, sizeof(HANDSHARK_MSG), 0);/* 从服务器循环接收消息 */while(1){pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);pthread_testcancel();   // 将接收缓冲区buffer清空bzero(&buffer,sizeof(buffer));// 接收服务器信息printf("Receiving messages from client %d ...\n", _socketInfo.socketCon);printf("---------------------------------------------\n");buffer_length = recv(_socketInfo.socketCon, buffer, BUFSIZ, 0);if(buffer_length == 0){// 判断为客户端退出printf("%s:%d Closed!\n", _socketInfo.ipaddr, _socketInfo.port);// 找到该客户端在数组中的位置for(con = 0; con < conClientCount; con++){if(arrConSocket[con].socketCon == _socketInfo.socketCon){break;}}// 将该客户端的信息删除,重置客户端数组for(i = con; i < conClientCount-1; i++){arrConSocket[i] = arrConSocket[i+1];}conClientCount --;break;}else if(buffer_length < 0){printf("Fail to call read()\n");break;}buffer[buffer_length] = '\0';if (buffer_length <= 11){printf("\nmessage is error\n");}else{printf("%s:%d said:%s\n", _socketInfo.ipaddr, _socketInfo.port, buffer);ReadToSend = 1;     // 发送标志置位,允许主线程发送数据usleep(100000);}}printf("%s:%d Exit\n", _socketInfo.ipaddr, _socketInfo.port);return NULL;
}/********************************************************************
*   Function Name: checkThrIsKill(pthread_t thr)
*   Description: 检测当前线程是否存活.
*   Called By: server.c[main]
*   Input: thr -> 线程数组中的线程
*   Date: 2021/09/13
*********************************************************************/
int checkThrIsKill(pthread_t thr){int res = 1;int res_kill = pthread_kill(thr, 0);if(res_kill == 0){res = 0;}return res;
}

mysocket.h

/********************************************************************
*   File Name: mysocket.h
*   Description: 用于实现多客户端通信
*   Init Date: 2021/09/13
*********************************************************************/
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>//#define SERVER_IP "127.0.0.1"       // 用于本地测试
#define SERVER_IP "192.168.43.44" // 用于公网测试
//#define SERVER_PORT 35607
#define HANDSHARK_MSG "Hello,Client!\n"
#define MaxClientNum 3/* 套接字信息结构体,用于记录客户端信息 */
typedef struct MySocketInfo{int socketCon;                  // 套接字描述符char *ipaddr;                   // 客户端IP地址uint16_t port;                  // 客户端端口号
}_MySocketInfo;char buffer[BUFSIZ];                // 服务器数据收发缓冲区
int ReadToSend;                     // 服务器准备发送标志位/* 用于记录客户端信息的数组 */
struct MySocketInfo arrConSocket[MaxClientNum];
int conClientCount;                 // 当前客户端数量/* 用来与客户端通信的线程数组 */
pthread_t arrThrReceiveClient[MaxClientNum];
int thrReceiveClientCount;          // 当前通信子线程数量/* 线程功能函数 */
void *fun_thrReceiveHandler(void *socketInfo);
void *fun_thrAcceptHandler(void *socketListen);
int checkThrIsKill(pthread_t thr);

usart.c

#include"../inc/usart.h"void *usart_handler(void*arg){//接受传递的参数struct argument *arg_th;arg_th = (struct argument *)arg;OpenSerialPort(&arg_th->serial_fd,arg_th->serial_port);    //打开串口if(InitSerialPort(&arg_th->serial_fd,arg_th->USART_BPS, 8, 1, 'N') == -1)            //初始化串口{printf("fail to InitSerialPort!");}while (1){//    printf("wohao \n");read_num = SerialPortReadData(&arg_th->serial_fd,buf, 100);if (read_num > 0){UarstToSend = 1;printf("buf is %s\n",buf);SerialPortWriteData(&arg_th->serial_fd,buf, strlen(buf));}sleep(2);}return 0;
}/**************************************************************** @name OpenSerialPort()* @function 打开串口设备* @para   NULL* @return 打开成功返回0,失败则返回-1* *************************************************************/
int OpenSerialPort(int *serial_fd,char *serial_port)
{*serial_fd = open(serial_port, O_RDWR|O_NOCTTY|O_NDELAY); //以读写、不允许进程管理串口、非阻塞打开串口(若打开成功返回文件描述符,否则返回-1)printf("%d,serial_port,%s\n",*serial_fd,serial_port);if(*serial_fd == -1){printf("open %s failed \n", serial_port);return -1;}else{printf("open %s success \n", serial_port);}/*设置串口设备文件为阻塞状态*/if(fcntl(*serial_fd,F_SETFL,0) < 0){printf("recover %s to block state failed \n", serial_port);}else{printf("%s is block state \n", serial_port);}/*测试是否为终端设备*/if (isatty(STDIN_FILENO) == 0){printf("standard input is not a terminal device");}else{printf("is a terminal device!");}return 0;
}/******************************************************************* @name InitSerialPort()* @function 串口初始化,设置波特率、数据位、奇偶校验位、停止位* @para nBaud:串口波特率(4800、9600、115200)*        nBits:数据位(5、6、7、8)*         nStop:停止位(1、2)*       Parity:奇偶校验位('O','E','N')* @return 初始化成功则返回0,失败则返回-1* ****************************************************************/
int InitSerialPort(int *serial_fd,int nBaud, int nBits, int nStop, char Parity)
{struct termios usart_config;/*先获取串口的原有配置*/if(tcgetattr(*serial_fd, &usart_config) != 0){printf("get usart_config failed \n");return -1;}memset(&usart_config, 0, sizeof(usart_config)); //清零/*设置波特率*/usart_config.c_cflag |= (CLOCAL|CREAD);       //设置本地模式、使能接收switch(nBaud){case 4800:cfsetispeed(&usart_config, B4800); //设置输入速率cfsetospeed(&usart_config, B4800);  //设置输出速率break;case 9600:cfsetispeed(&usart_config, B9600);  //设置输入速率cfsetospeed(&usart_config, B9600);  //设置输出速率break;case 115200:cfsetispeed(&usart_config, B115200);  //设置输入速率cfsetospeed(&usart_config, B115200);    //设置输出速率break;default:cfsetispeed(&usart_config, B9600);    //未设置则默认为9600cfsetospeed(&usart_config, B9600); //未设置则默认为9600}/*设置数据位*/usart_config.c_cflag &= ~CSIZE;     //清零c_cflag中数据位数大小switch(nBits){case 5:usart_config.c_cflag |= CS5;    //设置数据位为5位break;case 6:usart_config.c_cflag |= CS6;    //设置数据位为6位break;case 7:usart_config.c_cflag |= CS7;    //设置数据位为7位break;case 8:usart_config.c_cflag |= CS8;    //设置数据位为8位break;default:usart_config.c_cflag |= CS8;   //未设置则默认为8位}/*设置停止位*/if (nStop == 1){usart_config.c_cflag &= ~CSTOPB;    //设置1位停止位}else if (nStop == 2){usart_config.c_cflag |= CSTOPB;       //设置2位停止位}else{usart_config.c_cflag &= ~CSTOPB;    //若为其他数字则默认设置为1位停止位}/*设置奇偶校验位*/switch(Parity){case 'N':usart_config.c_cflag &= ~PARENB;  //禁止奇偶校验位(不使能奇偶校验位)break;case 'O':usart_config.c_cflag |= PARENB;      //使能校验位usart_config.c_cflag |= PARODD;     //设置奇校验usart_config.c_iflag |= (INPCK | ISTRIP);break;case 'E':usart_config.c_cflag |= PARENB;      //使能校验位usart_config.c_cflag |= ~PARODD;    //设置偶校验位usart_config.c_iflag |= (INPCK | ISTRIP);break;default:usart_config.c_cflag &= ~PARENB;   //禁止奇偶校验位(不使能奇偶校验位)}usart_config.c_cc[VTIME] = 0;    //VTIME:设置读取字符的usart_config.c_cc[VMIN] = 0; //指定要读取的最小字符数tcflush(*serial_fd, TCIOFLUSH);    //清空输入输出缓存if(tcsetattr(*serial_fd, TCSANOW, &usart_config) != 0){printf("usart set failed \n");return -1;}printf("usart set success: \n");return 0;
}/********************************************************************** @name SerialPortReadData()* @function 读取串口的数据* @para *readBuff:读取数组首地址*       dataLen:读取多少个字节* @return 返回读取的字节数目* @note 注意在使用时最好加上延时,对于目前此种读法好像会立即返回* ******************************************************************/
int SerialPortReadData(int *serial_fd,char *readBuff, size_t dataLen)
{int character_num=0;character_num = read(*serial_fd, readBuff, dataLen); //读取数据return character_num;
}/********************************************************************** @name SerialPortWriteData()* @function 向串口写数据* @para *writeBuff:写入数组首地址*          dataLen:写入多少个多少个字节* @return 返回读取的字节数目* ******************************************************************/
size_t SerialPortWriteData(int *serial_fd,char *writeBuff, size_t dataLen)
{int character_num=0;character_num = write(*serial_fd, writeBuff, dataLen);   //写入数据return character_num;
}/******************************************************************** @name SerialPortClose()* @fuction 关闭串口* *****************************************************************/
int SerialPortClose(int *serial_fd)
{return close(*serial_fd);
}

usart.h

#ifndef _USART_H_
#define _USART_H_/***************************************************************************** 传入设备文件路径* 然后打开串口* 再次初始化串口参数* 之后便可以利用SerialPortReadData,SerialPortWriteData读写* *************************************************************************/
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
#include <sys/select.h>char buf[100]={0};
int read_num=0,fd=-1;
int UarstToSend = 0;struct argument
{int serial_fd;char *serial_port;int USART_BPS;
}arg1,arg2;int OpenSerialPort(int *serial_fd,char *serial_port);//打开串口函数
int InitSerialPort(int *serial_fd,int nBaud, int nBits, int nStop, char Parity);    //设置串口参数
int SerialPortReadData(int *serial_fd,char *readBuff, size_t dataLen);      //读取串口数据
size_t SerialPortWriteData(int *serial_fd,char *writeBuff, size_t dataLen); //写入串口数据
int SerialPortClose(int *serial_fd);
int GetSerialPort_fd();     //获取串口设备描述符
//int readDataTty(char *rcv_buf, int TimeOut, int Len);
void *usart_handler(void * arg);#endif

02_C++实现多线程服务器代码(linux系统)相关推荐

  1. linux系统安装服务器过程,Linux系统服务器安装宝塔面板图文过程详解

    现在网站大多数都是用PHP开发的,特别是些CMS系统,例如织梦和wordpress这些. 既然用PHP开发,那么服务器用Linux系统是最合适不过(当然你用Windows也是可以的). linux系统 ...

  2. 电脑装服务器系统好处,服务器选用Linux系统的几个好处

    服务器选用Linux系统的几个好处: 1.开源 Linux系统可用于开源用途,通过开源,使用者不仅可以查看Linux内核的代码,还可以对代码进行修改和搭建. 2.稳定性 使用Linux系统的用户很少会 ...

  3. 联想服务器的系统安装教程视频教程,联想服务器装linux系统怎么安装教程

    联想服务器装linux系统怎么安装教程 [2021-02-11 17:09:22]  简介: 服务器 对于不会给电脑装系统的人,想学习Linux可在虚拟机上安装Linux操作系统进行学习,或者去找网页 ...

  4. 云服务器 ECS Linux 系统下使用 dig 命令查询域名解析

    云服务器 ECS Linux 系统可以使用通常自带的 dig 命令来查询域名解析情况.本文对此进行简要说明. 查询域名 A 记录 命令格式: dig <域名> 比如,查询域名 www.al ...

  5. h3c服务器u盘启动装linux系统,h3c服务器装Linux系统

    h3c服务器装Linux系统 内容精选 换一换 华为云上提供从8GB到512GB多款规格的SAP弹性云服务器,这些云服务器均经过SAP官方认证,可应用于开发.测试和生产系统.详情请参见SAP S/4H ...

  6. boa服务器 系统设置,boa服务器在linux系统下搭建

    boa服务器在linux系统下搭建 内容精选 换一换 制作Docker镜像,有以下两种方法.快照方式制作镜像(偶尔制作的镜像):在基础镜像上,比如Ubuntu,先登录镜像系统并安装Docker软件,然 ...

  7. linux监听apache代码,linux系统使用python监控apache服务器进程脚本分享

    这篇文章主要介绍了linux系统使用python监控apache服务器进程的脚本,大家参考使用吧 crtrl.py监控Apache服务器进程的Python 脚本 复制代码 代码如下: !/usr/bi ...

  8. linux多线程编写哲学家,Linux系统编程(三) ------ 多线程编程

    一.线程的创建和调度 1.线程是程序执行的某一条指令流的映像. 为了进一步减少处理机制的空转时间,支持多处理器及减少上下文切换开销,进程在演化中出现了另一个概念--线程.它是进程内独立的一条运行路线, ...

  9. linux web 服务器性能,Linux系统Web服务器性能测试(2)

    2.系统内存的影响 在linux系统下,有一点需要注意:对于某些BIOS来说,如果超过64MB内存就需要在LILO.CONF中加入扩展内存的命令,否则服务器内存显示只有64MB.我们对64MB内存和1 ...

最新文章

  1. Winform中使用用户控件实现带行数和标尺的RichTextBox(附代码下载)
  2. 大数据时代的数据存储,非关系型数据库MongoDB(一)(转)
  3. 计算机电路基础张志良,计算机电路基础
  4. 二叉树知识点最详细最全讲解
  5. vs下使用qt设置应用程序的图标
  6. Python《通过解析http请求搞定动态加载,爬取toutiao图片》
  7. go web db每次关闭_竟然不用写代码!一款可视化 Web 管理后台生成工具
  8. MyBatis 简介、 环境搭建、数据库连接池、查询方式
  9. 【封装那些事】 泄露的封装
  10. 熬夜整理了10个行业的40份可视化大屏模板,可直接拿走套用
  11. 【WC2013】糖果公园
  12. Burp Suite CA证书下载及导入教程
  13. javaSE (四十二)javaSE阶段性总结
  14. 第一章 Python数据模型
  15. 基础SQL第无课---数据修改
  16. 访问网页出现503服务器,503错误,手把手教你网页出现503错误怎么解决
  17. AliOS Things入门(1) 基于STM32L4与MDK搭建AliOS Things2.1.0开发环境
  18. 果断型性格分析,果断型人格的职业发展
  19. ksoftirqd内核线程
  20. 羊毛出在狗身上让猪来买单 - 智能音箱背后的平台经济

热门文章

  1. 神州信息2020:金融自主创新、金融超脑、场景金融三箭齐发
  2. 【sdx62】QCMAP_CLI手动拨号操作说明
  3. 数据使用要谨慎——不良数据带来严重后果
  4. 线性代数方程组数值解法
  5. 计算机话筒技术指标,手把手教你搞懂麦克风的技术指标
  6. VIDEOIO ERROR: V4L/V4L2: VIDIOC_S_CROP
  7. 【时间序列分析】12.MA(q)模型
  8. 一小时让你成为点云建图小将(固定帧数法选取关键帧)
  9. 解码百度核心:移动的无色墙,AI的无形剑
  10. 如何发掘各种暴利的赚钱项目,如何知道别人在干什么赚钱