linux socket 聊天室
linux socket 聊天室,本来这并不是我自己要做的,只是为了帮别人完成作业。刚好最近这段时间的课是关于socket编程,何不拿来练练手?
基于socket的聊天室早在开学初就有做过类似的,只不过当时用的java来实现,而且因为没有正式学过socket,代码只是搬用别人的,并没有深入理解。
单用户-服务的对话还是很好实现的,即使是多用户-服务,只要不是连续服务,服务端还是可以通过轮询的方式服务多个用户。问题就在于,常用socket I/O函数大都是阻塞的,这就意味着,单个线程只能服务于一个用户。于是自然而然的想到用多线程,然而多线程并不是最佳的解决方案,毕竟如果频繁的创建和销毁线程会造成一定的浪费。而利用select的多路复用,就能更好的解决。
客户端实现
客户端是很好实现的,只有2个I/O要通信,server socket和stdin,并且是fd值固定的,maxfdp直接取socket+1就行了。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <stdbool.h> 5 #include <unistd.h> 6 #include <sys/socket.h> 7 #include <arpa/inet.h> 8 9 #define BUF_SIZE 256 // 缓冲区长度 10 #define STDIN 0 // stdinfd 11 #define STDOUT 1 // stdoutfd 12 #define INVALID -1 13 14 /** 负责 socket 的初始化,连接到服务器 */ 15 int socket_setup(const char *serv_ip, int serv_port) 16 { 17 int rtn, sockfd = socket(AF_INET, SOCK_STREAM, 0); 18 struct sockaddr_in sockaddr; 19 20 bzero(&sockaddr, sizeof(sockaddr)); 21 sockaddr.sin_family = AF_INET; 22 sockaddr.sin_port = htons(serv_port); 23 inet_pton(AF_INET, serv_ip, &sockaddr.sin_addr); 24 25 rtn = connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)); 26 27 if (rtn == INVALID) 28 { 29 puts("Connection failure"); 30 exit(1); 31 } 32 else 33 { 34 puts("Connection successful"); 35 return sockfd; 36 } 37 } 38 39 int main(int argc, const char *argv[]) 40 { 41 int i, read_size, sockfd = socket_setup(argv[1], atoi(argv[2])); 42 char buffer[BUF_SIZE]; 43 fd_set fdset; 44 45 while (true) 46 { 47 FD_ZERO(&fdset); 48 FD_SET(STDIN, &fdset); 49 FD_SET(sockfd, &fdset); 50 select(sockfd + 1, &fdset, NULL, NULL, NULL); 51 52 /** socket -> 标准输出 */ 53 if (FD_ISSET(sockfd, &fdset)) 54 { 55 read_size = read(sockfd, buffer, BUF_SIZE); 56 write(STDOUT, buffer, read_size); 57 58 if (read_size == 0) 59 { 60 puts("Server close"); 61 exit(1); 62 } 63 } 64 65 /** 标准输入 -> socket */ 66 if (FD_ISSET(STDIN, &fdset)) 67 { 68 read_size = read(STDIN, buffer, BUF_SIZE); 69 write(sockfd, buffer, read_size); 70 } 71 } 72 73 return 0; 74 }
服务端实现
这个聊天室的实现难点是服务端,要能支持多用户聊天。
我的实现方法是,用一个结构体数组clients保存已连接的客户信息,使用遍历数组的方式广播消息,一般来说都能想到这点,重点是利用select实现多路复用。
因为客户socket是动态的,必须小心处理客户的连接/断开,当有用户连接时保存其socket,断开连接后将socket值为无效。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <stdbool.h> 5 #include <unistd.h> 6 #include <time.h> 7 #include <sys/socket.h> 8 #include <arpa/inet.h> 9 10 #define TIME_SIZE 16 // 表示时间的字符串长度 11 #define IP_SIZE 16 // IP 字符串长度 12 #define BUF_SIZE 256 // 缓冲区大小 13 #define CLIENT_SIZE 8 // 允许的客户端数量 14 #define BACKLOG CLIENT_SIZE // listen 队列长度,等于允许的客户端数量 15 #define INVALID -1 16 17 /** 保存客户端连接信息结构体 */ 18 struct CLIENT { 19 int clientfd; 20 struct sockaddr_in sockaddr; 21 char ip[IP_SIZE]; 22 int port; 23 } clients[CLIENT_SIZE]; 24 25 /** 初始化客户端列表 */ 26 void init_clients(void) 27 { 28 int i; 29 30 for (i = 0; i< CLIENT_SIZE; i++) 31 clients[i].clientfd = INVALID; 32 } 33 34 /** 向每一个已连接的用户广播消息 */ 35 void broadcast(char *msg) 36 { 37 int i; 38 39 for (i = 0; i< CLIENT_SIZE; i++) 40 if (clients[i].clientfd != INVALID) 41 write(clients[i].clientfd, msg, strlen(msg)); 42 } 43 44 /** 格式化要发送的消息 */ 45 void strfmsg(int i, char *buffer, const char *msg) 46 { 47 char curtime[TIME_SIZE]; 48 time_t curtime_t; 49 struct tm *timeinfo; 50 51 time(&curtime_t); 52 timeinfo = localtime(&curtime_t); 53 strftime(curtime, TIME_SIZE, "%X", timeinfo); 54 55 sprintf( 56 buffer, 57 "<%s %s:%d> %s", 58 curtime, 59 clients[i].ip, 60 clients[i].port, 61 msg); 62 } 63 64 /** 新连接处理 */ 65 void accept_connect(int listenfd) 66 { 67 int connectfd, i; 68 char buffer[BUF_SIZE]; 69 struct sockaddr_in clientaddr; 70 socklen_t connectlen = sizeof(struct sockaddr_in); 71 72 connectfd = accept( 73 listenfd, 74 (struct sockaddr *)&clientaddr, 75 &connectlen); 76 77 /** 记录连接者信息 */ 78 for (i = 0; i < CLIENT_SIZE; i++) 79 { 80 if (clients[i].clientfd == INVALID) 81 { 82 clients[i].clientfd = connectfd; 83 memcpy(&clients[i].sockaddr, &clientaddr, connectlen); 84 clients[i].port = ntohs(clients[i].sockaddr.sin_port); 85 inet_ntop( 86 AF_INET, 87 &clients[i].sockaddr.sin_addr, 88 clients[i].ip, 89 IP_SIZE); 90 91 strfmsg(i, buffer, "login\n"); 92 printf("%s", buffer); 93 broadcast(buffer); 94 95 break; 96 } 97 } 98 99 /** 连接数超出 */ 100 if (i == CLIENT_SIZE) 101 { 102 strcpy(buffer, "Out of Number\n"); 103 write(connectfd, buffer, strlen(buffer)); 104 close(connectfd); 105 } 106 } 107 108 /** 客户端消息处理 */ 109 void chat(fd_set fdset) 110 { 111 int sockfd, read_size, i; 112 char read_buf[BUF_SIZE], send_buf[BUF_SIZE]; 113 114 for (i = 0; i < CLIENT_SIZE; i++) 115 { 116 sockfd = clients[i].clientfd; 117 118 if (sockfd != INVALID && FD_ISSET(sockfd, &fdset)) 119 { 120 read_size = read(sockfd, read_buf, BUF_SIZE - 1); 121 122 if (read_size == 0) 123 { 124 /** 失去连接 */ 125 close(sockfd); 126 clients[i].clientfd = INVALID; 127 128 strfmsg(i, send_buf, "logout\n"); 129 printf("%s", send_buf); 130 broadcast(send_buf); 131 132 continue; 133 } 134 else 135 { 136 read_buf[read_size] = '\0'; 137 strfmsg(i, send_buf, read_buf); 138 printf("%s", send_buf); 139 broadcast(send_buf); 140 } 141 } 142 } 143 } 144 145 /** 负责 socket 初始化,绑定监听端口 */ 146 int socket_setup(int port) 147 { 148 int rtn, listenfd = socket(AF_INET, SOCK_STREAM, 0); 149 struct sockaddr_in sockaddr; 150 151 bzero(&sockaddr, sizeof(sockaddr)); 152 sockaddr.sin_family = AF_INET; 153 sockaddr.sin_port = htons(port); 154 sockaddr.sin_addr.s_addr= htonl(INADDR_ANY); 155 156 rtn = bind(listenfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)); 157 158 if (rtn == INVALID) 159 { 160 puts("Bind failure"); 161 exit(1); 162 } 163 164 if (listen(listenfd, BACKLOG) == INVALID) 165 { 166 puts("Listen failure"); 167 exit(1); 168 } 169 170 puts("Service startup"); 171 return listenfd; 172 } 173 174 int main(int argc, const char *argv[]) 175 { 176 int maxfdp, i, listenfd = socket_setup(atoi(argv[1])); 177 fd_set fdset; 178 179 init_clients(); 180 181 while (true) 182 { 183 FD_ZERO(&fdset); 184 FD_SET(listenfd, &fdset); 185 maxfdp = listenfd; 186 187 /** 将可用的客户端 socket 加入 fdset,并计算 maxfdp */ 188 for (i = 0; i < CLIENT_SIZE; i++) 189 { 190 if (clients[i].clientfd != INVALID) 191 { 192 FD_SET(clients[i].clientfd, &fdset); 193 194 if (clients[i].clientfd > maxfdp) 195 maxfdp = clients[i].clientfd; 196 } 197 } 198 199 select(maxfdp + 1, &fdset, NULL, NULL, NULL); 200 201 if (FD_ISSET(listenfd, &fdset)) 202 accept_connect(listenfd); 203 204 chat(fdset); 205 } 206 207 return 0; 208 }