实现目标

【1】创建TCP服务器和客户端,实现简易聊天程序;
【2】单一进程,通过I/O复用select函数实现;
【3】客户端/服务器任一结束,结束连接和对方进程。

select函数

Linux系统中I/O复用实现方式有selece()、poll()、epoll()。I/O复用可以使单一进程可以同时监听多个文件描述符(句柄)事件的发生,而不用建立多个进程,在特殊情况下可以节约系统资源,但牺牲一定的效率。 常用的I/O复用场景有:

【1】服务端同时处理监听socket和连接socket ;
【2】服务端同时处理TCP请求和UDP请求 ;
【3】服务端同时监听多个端口或者处理多种服务请求;
【4】客户端同时处理多个socket ;
【5】客户端同时处理用户输入(如STDIN)和网络连接。

select函数功能是在指定超时时间内,或者阻塞,监听进程指定的文件描述符上的事件状态,在可读、可写、异常事件发生后,唤醒进程进行相关事件处理。

函数原型

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

形参
【Linux网络编程】TCP select聊天程序_客户端
返回值
【Linux网络编程】TCP select聊天程序_udp_02
select执行失败时,常见错误码
【Linux网络编程】TCP select聊天程序_客户端_03
相关函数
【Linux网络编程】TCP select聊天程序_客户端_04
使用注意事项

【1】select函数调用后,会清空它所监听的文件描述符集合,所以每次调用select()之前必须把描述符重新加入到监听集合中;
【2】最大监听文件描述符有限制,一般是1024。

实现代码

服务器端(server)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/prctl.h>
#include <arpa/inet.h>
#include <errno.h>
#include <resolv.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>

int main (int argc, char * argv[])
{
int s_fd = 0, c_fd = 0, pid;
socklen_t addr_len;
struct sockaddr_in s_addr, c_addr;
char buf[1024];
ssize_t size = 0;
fd_set s_fds;
int max_fd; /* 监控文件描述符中最大的文件号 */
struct timeval tv; /* 超时返回时间 */
int ret = 0;

if(argc != 3)
{
printf("format error!\n");
printf("usage: server <address> <port>\n");
exit(1);
}

/* 服务器端地址 */
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
if(!inet_aton(argv[1], (struct in_addr *) &s_addr.sin_addr.s_addr))
{
perror("invalid ip addr:");
exit(1);
}

/* 创建socket */
if((s_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket create failed:");
exit(1);
}

/* 端口重用,调用close(socket)一般不会立即关闭socket,而经历“TIME_WAIT”的过程 */
int reuse = 0x01;
if(setsockopt(s_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(int)) < 0)
{
perror("setsockopt error");
close(s_fd);
exit(1);
}

/* 绑定地址 */
if(bind(s_fd, (struct sockaddr*)&s_addr, sizeof(s_addr)) < 0)
{
perror("bind error");
close(s_fd);
exit(1);
}

/* 监听socket */
if(listen(s_fd, 5) < 0)
{
perror("listen error");
close(s_fd);
exit(1);
}
addr_len = sizeof(struct sockaddr);
int set = 0;
for(;;)
{
FD_ZERO(&s_fds); /* 清空s_fds集合 */
FD_SET(STDIN_FILENO, &s_fds); /* 加入标准输入集合 */
FD_SET(s_fd, &s_fds); /* 加入服务器fd集合 */
if(c_fd != 0)
{
FD_SET(c_fd, &s_fds); /* 加入客户端fd集合 */
}
if(c_fd >= max_fd)
{
max_fd = c_fd;
}
else
{
max_fd = s_fd;
}
tv.tv_sec = 10;
tv.tv_usec = 0;

ret = select(max_fd+1, &s_fds, NULL, NULL, &tv);

if(ret < 0)
{
perror("select error");
break;
}
else if(ret == 0)
{
continue; /* time out */
}

if(FD_ISSET(s_fd,&s_fds))
{
if(c_fd)
{
continue;
}
c_fd = accept(s_fd, (struct sockaddr*)&c_addr, (socklen_t *)&addr_len);

if(c_fd < 0)
{
perror("accept error");
close(s_fd);
exit(1);
}
else
{
printf("connected with ip: %s and port: %d\n", inet_ntop(AF_INET,&c_addr.sin_addr, buf, 1024), ntohs(c_addr.sin_port));
}
printf("waiting client connecting\r\n");
}

if(c_fd <= 0)
{
continue; /* 无客户端连接 */
}

if(FD_ISSET(c_fd,&s_fds))
{
memset(buf, 0, sizeof(buf));
size = read(c_fd, buf, sizeof(buf) - 1);
if(size > 0)
{
printf("message recv %dByte: \n%s\n",size,buf);
}
else if(size < 0)
{
printf("recv failed!errno code is %d,errno message is '%s'\n",errno, strerror(errno));
break;
}
else
{
printf("client disconnect!\n");
break;
}
}

if(FD_ISSET(STDIN_FILENO,&s_fds))
{
fflush(stdout);
memset(buf, 0, sizeof(buf));
size = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if(size > 0)
{
buf[size-1] = '\0';
}
else if(size == 0)
{
printf("read is done...\n");
break;
}
else
{
perror("read stdin error");
break;
}
if(!strncmp(buf, "quit", 4))
{
printf("close the connect!\n");
break;
}
if(buf[0] == '\0')
{
printf("please enter message to send:\n");
continue;
}
size = write(c_fd, buf, strlen(buf));
//printf("write %s\n,size %d",buf,strlen(buf));
if(size <= 0)
{
printf("message'%s' send failed!errno code is %d,errno message is '%s'\n",buf, errno, strerror(errno));
break;
}
printf("please enter message to send:\n");
}
}

close(s_fd);
close(c_fd);

return 0;
}

客户端(client)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/prctl.h>

int main (int argc, char * argv[])
{
int c_fd,pid;
int ret = 0;
struct sockaddr_in s_addr;
socklen_t addr_len;
char buf[1024];
ssize_t size;
fd_set s_fds;
int max_fd; /* 监控文件描述符中最大的文件号 */
struct timeval tv; /* 超时返回时间 */

if(argc != 3)
{
printf("format error!\n");
printf("usage: client <address> <port>\n");
exit(1);
}

/* 创建socket */
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd < 0)
{
perror("socket create failed");
return -1;
}

/* 服务器端地址 */
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
if(!inet_aton(argv[1], (struct in_addr *) &s_addr.sin_addr.s_addr))
{
perror("invalid ip addr");
exit(1);
}

/* 连接服务器*/
addr_len = sizeof(s_addr);
ret = connect(c_fd, (struct sockaddr*)&s_addr, addr_len);
if(ret < 0)
{
perror("connect server failed");
exit(1);
}

for(;;)
{
FD_ZERO(&s_fds); /* 清空s_fds集合 */
FD_SET(STDIN_FILENO, &s_fds); /* 加入标准输入集合 */
FD_SET(c_fd, &s_fds); /* 加入客户端fd集合 */
max_fd = c_fd;

tv.tv_sec = 30;
tv.tv_usec = 0;

ret = select(max_fd+1, &s_fds, NULL, NULL, &tv);

if(ret < 0)
{
perror("select error");
break;
}
else if(ret == 0)
{
continue; /* time out */
}
if(FD_ISSET(STDIN_FILENO,&s_fds)) /* 终端消息 */
{
fflush(stdout);
memset(buf, 0, sizeof(buf));
size = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if(size > 0)
{
buf[size-1] = '\0';
}
else if(size == 0)
{
printf("read is done...\n");
break;
}
else
{
perror("read stdin error");
break;
}
if(!strncmp(buf, "quit", 4))
{
printf("close the connect!\n");
break;
}
if(buf[0] == '\0')
{
printf("please enter message to send:\n");
continue;
}
size = write(c_fd, buf, strlen(buf));
//printf("write %s\n,size %d",buf,strlen(buf));
if(size <= 0)
{
printf("message'%s' send failed!errno code is %d,errno message is '%s'\n",buf, errno, strerror(errno));
break;
}
printf("please enter message to send:\n");
}

if(FD_ISSET(c_fd,&s_fds)) /* 服务器消息 */
{
memset(buf, 0, sizeof(buf));
size = read(c_fd, buf, sizeof(buf));
if(size > 0)
{
printf("message recv %dByte: \n%s\n",size,buf);
}
else if(size < 0)
{
printf("recv failed!errno code is %d,errno message is '%s'\n",errno, strerror(errno));
break;
}
else
{
printf("server disconnect!\n");
break;
}
}

}

close(c_fd);

return 0;
}

执行效果
【Linux网络编程】TCP select聊天程序_udp_05