简易Linux终端聊天室
今天我们来实现一个简单的小项目,在这个项目中,我们将实现一个终端版的简易Linux聊天室。
实现的效果:服务器启动,监测客户端连接的个数,监测每个客户端的IP地址以及端口号,当每个客户端发送消息时,服务器上会有线程专门将每个客户端发送的信息记录在界面上,就类似平时使用QQ群聊一样。我们来看看这个简易的Linux聊天室如何来实现吧。如图4-5-12所示。
1、实现一个基本的服务器和客户端的步骤
一、创建服务器的流程
(1)调用socket函数创建一个套接口,并返回描述符。
(2)调用bind函数使服务器进程与一个端口号绑定。
(3)调用listen设置客户端如队列的大小。
(4)调用accept接收一个连接,如果接入队列不为空的话。并且相应返回一个已连接的套接口描述符。
(5)调用send和recv用来在已连接的套接口间进行发送和接收数据。
二、创建客户端流程
(1)调用socket函数创建一个套接口,并返回描述符。
(2)调用connect向服务器发送连接请求,返回一个已连接的套接口。
(3)调用send和recv在已连接的套接口间发送和接收数据。
1.1服务器将要完成的工作
(1)获取套接字
(2)设置端口复用
(3)绑定连接的IP还有端口号
(4)监听
(5)创建一条线程用于显示客户端连接信息,具体连接的人数,顺便将客户连接的IP以及端口号打印出来。
(6)开始接收
(7)创建一条线程用于将客户端直接收发的信息分发到客户端处进行显示。
下面具体看看服务器代码的实现 server.c
1#include <stdio.h> 2#include <unistd.h> 3#include <sys/socket.h> 4#include <netinet/in.h> 5#include <netinet/ip.h> 6#include <string.h> 7#include <pthread.h> 8#include <sys/time.h> 9//设置客户端最大的个数为40个 10#define MAXCONNECTION 40 11#define msleep(x) (usleep(x*1000)) 12struct Data 13{ 14 int live ; //0 无人用 1有人用 15 int sockfd ; 16 struct in_addr in ; 17 unsigned short port ; 18}; 19 20struct Data array[MAXCONNECTION] = {0} ; 21void *do_thread_showconnect(void *arg); 22void *do_thread_clientopt(void *arg); 23int main(void) 24{ 25 int sockfd ; 26 //1.获取套接字 27 sockfd = socket(AF_INET , SOCK_STREAM , 0); 28 if(sockfd < 0) 29 { 30 perror("get socket fail"); 31 return -1 ; 32 } 33 //2.设置端口复用 34 int on = 4 ; 35 setsockopt(sockfd , SOL_SOCKET , SO_REUSEADDR , &on , sizeof(int)); 36 //3.绑定IP与端口 37 struct sockaddr_in addr ; 38 addr.sin_family = AF_INET ; 39 addr.sin_port = htons(10086); 40 //设置为INADDR_ANY,表示监听所有的。 41 addr.sin_addr.s_addr = INADDR_ANY ; 42 int ret ; 43 ret = bind(sockfd , (struct sockaddr *)&addr , sizeof(struct sockaddr_in)) ; 44 if(ret < 0) 45 { 46 perror("bind error"); 47 return -2 ; 48 } 49 //4.监听 50 listen(sockfd , 30); 51 int peersockfd ; 52 struct sockaddr_in peeraddr ; 53 socklen_t len = sizeof(struct sockaddr_in) ; 54 struct Data tmp ; 55 char *message = "too more connction , connect fail" ; 56 int i ; 57 pthread_t tid ; 58 //创建一条线程用于显示已连接的客户端的个数 59 pthread_create(&tid , NULL , do_thread_showconnect , NULL); 60 pthread_detach(tid); 61 62 while(1) 63 { 64 peersockfd = accept(sockfd , (struct sockaddr *)&peeraddr , &len); 65 if(peersockfd < 0) 66 { 67 perror("accept fail"); 68 return -3 ; 69 } 70 tmp.sockfd = peersockfd ; 71 tmp.in = peeraddr.sin_addr ; 72 tmp.port = ntohs(peeraddr.sin_port); 73 tmp.live = 1 ; 74 75 for(i = 0 ; i < MAXCONNECTION ; i++) 76 { 77 if(array[i].live == 0) 78 { 79 array[i] = tmp ; 80 break; 81 } 82 } 83 //判断是否连接个数已满 84 if(i == MAXCONNECTION) 85 { 86 write(peersockfd , message , strlen(message)); 87 close(peersockfd); 88 continue ; 89 } 90 //创建一条线程用于显示客户端之间互相发送的即时信息 91 pthread_create(&tid , NULL , do_thread_clientopt , (void *)i); 92 pthread_detach(tid); 93 } 94 95 96 return 0 ; 97} 98 99//线程执行函数,用于显示已连接的客户端的个数。 100void *do_thread_showconnect(void *arg) 101{ 102 int i , count = 0; 103 while(1) 104 { 105 system("clear"); 106 printf("客户端连接信息: 连接人数:%d\n" , count); 107 count = 0 ; 108 for(i = 0 ; i < MAXCONNECTION ; i++) 109 { 110 if(array[i].live == 1) 111 { 112 count++ ; 113 printf("IP:%s port:%d \n" , inet_ntoa(array[i].in) , array[i].port); 114 } 115 } 116 msleep(100); 117 } 118} 119//线程执行函数,用于显示客户端之间互相发送的即时信息 120void *do_thread_clientopt(void *arg) 121{ 122 //转发信息 123 int num = (int)arg ; 124 char buffer[12240] = {0}; 125 char tmp[10240] = {0}; 126 int ret ; 127 struct timeval tv ; 128 struct timezone tz ; 129 struct tm *tt ; 130 int i ; 131 132 while(1) 133 { 134 ret = read(array[num].sockfd , tmp , 1024); 135 if(ret <= 0) 136 break; 137 tmp[ret] = '\0' ; 138 gettimeofday(&tv , &tz); 139 tt = localtime(&tv.tv_sec); 140 sprintf(buffer , "%s @ %d:%d:%d :\n%s" ,inet_ntoa(array[num].in) , tt->tm_hour , tt->tm_min , tt->tm_sec , tmp); 141 142 for(i = 0 ; i < MAXCONNECTION ; i++) 143 { 144 if(array[i].live == 1) 145 { 146 write(array[i].sockfd , buffer , strlen(buffer)); 147 } 148 } 149 } 150 close(array[num].sockfd); 151 array[num].live = 0 ; 152 153}
服务端的工作已经设置完毕,显示就开始设置客户端吧,客户端就可以把它想象成我们的QQ群聊,只要每个人一发信息,那么整个群都可以看得到。
1.2客户端将要完成的工作
(1)连接对应的服务器,必须指定服务器的ip地址
(2)创建一条线程,用于读取从服务器转发过来的消息
(3)客户端可以自由的输入,通过服务器,发送给其它的客户端,让它们也可以看得到。
下面具体看看客户端代码的实现 client.c
1#include <stdio.h> 2#include <sys/socket.h> 3#include <netinet/in.h> 4#include <netinet/ip.h> 5#include <pthread.h> 6void *do_thread(void * arg); 7int main(void) 8{ 9 int sd ; 10 11 sd = socket(AF_INET , SOCK_STREAM , 0); 12 if(sd < 0) 13 { 14 perror("get socket fail"); 15 return -1 ; 16 } 17 //1.连接对应的服务器 18 //connect 19 struct sockaddr_in addr ; 20 addr.sin_family = AF_INET ; 21 addr.sin_port = htons(10086); 22 addr.sin_addr.s_addr = inet_addr("10.126.72.56"); 23 24 int ret ; 25 ret = connect(sd , (struct sockaddr *)&addr , sizeof(struct sockaddr_in)); 26 if(ret != 0) 27 { 28 perror("connect fail"); 29 return -3 ; 30 } 31 printf("connect success ... \n"); 32 pthread_t tid ; 33 //创建一条线程用于接收从服务器端收到的数据 34 pthread_create(&tid , NULL , do_thread , (void *)sd); 35 pthread_detach(tid); 36 37 char buffer[1024] = {0}; 38 39 while(1) 40 { 41 //阻塞从标准输出读取信息到buffer 42 ret = read(0 , buffer , 1024); 43 if(ret > 1024) 44 continue ; 45 //按下回车后将buffer中的内容写到文件描述符 46 //通过服务器转发给其它正在连接的客户端 47 write(sd, buffer , ret); 48 } 49 50 return 0 ; 51} 52void *do_thread(void * arg) 53{ 54 int sd = (int)arg; 55 int ret ; 56 char buffer[1024] = {0}; 57 while(1) 58 { 59 //从服务器读取数据并显示在客户端上 60 ret = read(sd , buffer , 1024); 61 if(ret <= 0) 62 break; 63 buffer[ret] = '\0' ; 64 printf("recv:%s" , buffer); 65 } 66}
源码编写完毕,接下来测试一下这个简单聊天室的功能:编译过程省略,注意,该程序在32位操作系统上运行,且要加上线程库才可以编译成功。分别编译server.c和client.c
1gcc server.c -o server -m32 -lpthread 2gcc client.c -o client -m32 -lpthread
下面先运行服务器,执行./server如图4-5-13所示。
下面启动不同IP的客户端,找多一台电脑即可测试。在我方47服务器上执行客户端./client,如图4-5-14所示。客户端连接成功了!
接下来看看服务器上有什么变化,如图4-5-15所示。
在我方56服务器上执行客户端./client,如图4-5-16所示。
接下来看看服务器上有什么变化,如图4-5-17所示。
在47服务器上的客户端发送一条消息给56服务器上的客户端,同样的在56服务器上的客户端也发送一条信息给47的服务器上的客户端,观察变化,如图4-5-18所示。
在这里我们看到,56服务器上的客户端发送hello world的消息给47服务器上的客户端,47服务器上的客户端也收到了helloworld消息,同样的,47服务器上的客户端给56服务器上的客户端发送I am 47 server的消息,56服务器上的客户端也收到了I am 47 server的消息。这个简易版本的Linux聊天室就算完成了,接下来,请读者发挥自己的想象力,结合VT100控制码,写出一个更漂亮的终端版聊天工具吧。
VT100控制码表
1具体格式有两种, 2• 一种数字形式, 3\033[<数字>m . 4如 \033[40m ,表示让后面字符输出用背景黑色输出 \033[0m 表示取消前面的设置。 5• 另一种是控制字符形式。 6\033[K 清除从光标到行尾的内容 7\033[nC 光标右移 n 行 8输出时, 也可以用 ^[来代替. 9VT100 控制码 10VT100 控制码归类如下。 11\033[0m 关闭所有属性 12\033[1m 设置高亮度 13\033[4m 下划线 14\033[5m 闪烁 15\033[7m 反显 16\033[8m 消隐 17\033[30m -- \033[37m 设置前景色 18\033[40m -- \033[47m 设置背景色 19\033[nA 光标上移 n 行 20\033[nB 光标下移 n 行 21\033[nC 光标右移 n 行 22\033[nD 光标左移 n 行 23\033[y;xH 设置光标位置 24\033[2J 清屏 25\033[K 清除从光标到行尾的内容 26\033[s 保存光标位置 27\033[u 恢复光标位置 28\033[?25l 隐藏光标 29\033[?25h 显示光标 30VT100 关于颜色的说明: 31VT100 的颜色输出分为,注意要同时输出前景的字符颜色和背景颜色。 32字背景颜色范围:40----49 3340:黑 3441:深红 3542:绿 3643:黄色 3744:蓝色 3845:紫色 3946:深绿 4047:白色 41字颜色:30-----------39 4230:黑 4331:红 4432:绿 4533:黄 4634:蓝色 4735:紫色 4836:深绿 4937:白色 50这样输出一个字符串比较完整如下 51echo "\033[字背景颜色;字体颜色 m 字符串\033[0m" 52例: 53echo "\033[41;36m something here \033[0m"
1例如: 2C语言编程里可以这么用 3设置光标位置 x=1 y=2 4printf("\033[%d;%dH\033[43m \033[0m" ,1, 2);
下一节,我将带领大家使用VT100控制码来实现一个好玩的方块。