[toc]

本人博客https://qinzheng7575.github.io/

前言

在之前写的socket通信程序中,有一个服务器,一接收到消息就会回复一个ACK;有一个客户机,一直等待输入按下回车就可发送。这似乎已经很完美了,能够实现通信,但是,稍微想想它和我们日常使用的聊天程序QQ、微信之间的区别,就能够发现,哦,原来完全没法做到真正的“自由聊天”。

目标与关键技术

  • 编写两对简单的一对一聊天程序,分别为面向连接方式和无连接方式
  • 双方可以自由聊天,即随时都可以输入或显示对方的数据

要实现第二个,看起来很简单,其实需要引入全新的机制:多路复用机制

我们能不能不用非得等到对面回复了东西才能继续发?能不能在scanf阻塞等待输入的时候,我先接收对面的东西?要想避免线性流水线式的程序模型,就必须引入多路复用,而select机制,就是时分复用实现多路复用的形式。

select()

功能:检查多个套接字状态,将其放在对应的队列中,我们就可以根据不同的队列来进行操作了

意味着:每次只要是队列里的socket,就必定代表它发生了某种事情,导致了它在这个队列里面,那么我们接下来的操作肯定可以的

Linux一切皆文件

在Linux中,可以看到明显的编程风格区别,socket套接字不再是由SOCKET定义,而是int,其实就是因为,在操作系统看来一切都是文件,socket也不过是一个可以读、写等操作的文件描述符罢了。

同时,这也意味着,我们就可以直接把文件描述符0,1,2(标准输入、输出、错误),放到select()中,让输入文字也能够和接收socket消息并行了!

套接字队列

我们在select()中,需要填三个队列,分别为read_fdswrite_fdsexcept_fds。对套接字队列的操作,有初始化,插入,删除,查找,遍历等。而select()实际生就是帮我们把各个socket文件描述符,放到其该存在的位置,比如处于accpet后的套接字,一旦对方send了东西,那么它就变得可读了,经过select()后也就被放在了read_fds队列中。

当然,我们还需要手动创建一个套接字管理队列:因为套接字可能被从套接字队列中删除,需要一个班级的花名册来记录,方便下次再放进套接字队列中。

非阻塞

select我们要开启非阻塞模式,否则它就会阻塞掉,使得没法输入文字了,并且需要设置超时时间,避免忙等消耗掉计算机资源。

代码解析

完整代码请看本人的Github https://github.com/Qinzheng7575

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

#define STDIN 0//键盘输入文件描述符
struct socket_list {
//套接字管理队列结构
int MainSock;
int num;
int socket_array[256];
};
int main(){
int s, sock;//int instead SOCKET in Linux
struct sockaddr_in ser_addr, remote;
unsigned int len;
char buf[128];
struct socket_list sock_list;//我们建立的socket管理列表
fd_set readfds, writefds, exceptfds;//三种socket列表,对应读、写、意外
int i;
unsigned long arg;
struct timeval timeout;
int retval;//承接select、recv等函数的返回值
s = socket(AF_INET, SOCK_DGRAM, 0);//UDP
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_addr.sin_port = htons(0x1234);
bind(s, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
remote.sin_family = AF_INET;
remote.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
remote.sin_port = htons(0x4321);
timeout.tv_sec = 2;
timeout.tv_usec = 0;
init_list(&sock_list);
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
arg = 1;
int fd = STDIN;
//ioctlsocket(sock_list.MainSock, FIONBIO, &arg);//开启非阻塞for win
ioctl(s, FIONBIO, &arg);//开启非阻塞for linux
ioctl(fd, FIONBIO, &arg);
while (1) {
//建立那三个状态队列
FD_ZERO(&readfds);
insert_list(s, &sock_list);//it's important!!
make_fdlist(&sock_list, &readfds);
make_fdlist(&sock_list, &writefds);
make_fdlist(&sock_list, &exceptfds);
FD_SET(fd, &readfds);
retval = select(1024, &readfds, &writefds, &exceptfds, &timeout);
//printf("%d\n",retval);
//从套接字上的各种事件处理
//注意此处应设计为不断从套接字管理队列中逐个取出sock的循环模式

for (i = 0; i < 64; i++) {
if (sock_list.socket_array[i] == 0)continue;
sock = sock_list.socket_array[i];
//printf("%d,%d\n",i,sock);
//此时已经进入到套接字管理流程了,不需要再管新来的啥的了
if (FD_ISSET(STDIN, &readfds)) {
char in_buf[128];
read(STDIN, in_buf, 127);
len = sizeof(remote);
int a;
//a=sendto(s, "ACK", 3, 0, (sockaddr*)&remote, len);
if (strlen(in_buf) > 0) {
printf("have read from keyboard:%s\n", in_buf);
a = sendto(s, in_buf, strlen(in_buf), 0, (sockaddr*)&remote, len);
memset(in_buf, 0x00, sizeof(char) * 128);
printf("send ok %d\n", a);
}
}

if (FD_ISSET(sock, &readfds)) {
//len = sizeof(sock);
len = sizeof(remote);
int postion = 0;
postion = recvfrom(s, buf, 127, 0, (struct sockaddr*)&remote, &len);
buf[postion] = '\0';
if (strlen(buf) > 0) {
printf("接收到:%s\n", buf);
memset(buf, 0x00, sizeof(char) * 128);
//sendto(s, "ACK", 3, 0, (sockaddr*)&remote, len);
}
}
if (FD_ISSET(sock, &writefds)) {}
if (FD_ISSET(sock, &exceptfds)) {}
}
}
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
close(s);
return 0;
}

最后实现的,就是两个terminal能够自由的相对方发送数据,进行聊天!

通过测试界面我们可以看到,在一段打字的时候,并不会影响其接受数据,同时也能够任意发连续的文字,实现自由通信。