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

本文介绍了一款多客户端TCP服务器的实现,该服务器能够与串口设备通信,支持动态连接管理,具备透明传输和错误检测机制。服务器使用多线程处理客户端连接,当客户端数量超过预设阈值时,会随机关闭一个较早建立的连接。同时,服务器实现了自定义的私有协议,用于串口和客户端之间的双向通信。此外,文章还探讨了虚拟机环境下服务器的网络配置,以及串口通信的实现细节,包括串口的打开、初始化、读写和关闭操作。
摘要由CSDN通过智能技术生成

具体功能

  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编程,点击下面连接有详细讲解

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);	//未设置则默认为9600
			cfsetospeed(&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/lib

libyear.so.1.0:
		# 此为生成动态库文件
		# 将soname文件连接到真实动态库之中
        $(CC) -fpic -shared -Wl,-soname,$(SO_NAME) -o $(REAL_NAME) $(OBJS) -I$(SAVE_DIR) -lpthread

install:
		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 port
	printf("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);	//未设置则默认为9600
			cfsetospeed(&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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值