「网络程序设计」期末复习

本文最后更新于:2022年8月17日 上午

0 | 考点

四-六道手写代码大题, 非常硬核

由于得到的往年题资源较多, 而本课程知识比较分散, 故复习手段采用「以往年题为中心带出相关知识点」的方式

重点范围: tcp和udp服务器(包括基本模型,超时,地址可重用,字节顺序,僵尸,并发);高级套接字接收函数的几个选项;正向和反向代理原理;管道和有名管道;守护进程;udp广播;延迟创建子进程。

可省略部分

  1. 头文件不需要写
  2. bind(), connect(), accepct(), write(), sendto()等基本套接字函数不用写参数
  3. 不考虑EINTR的错误和read()返回0的情况
  4. 填写sockaddrsockaddr_in时, 可以用SET_ADDR_PORT单播和SET_ADDR_PORT_255广播代替

  5. char->int用CharToInt代替, int->char用IntToChar代替

  6. 重复代码块可以标上序号, 之后复用

1 | 2017年题目

题目1

考点

由于摄像机只有「一台」,故不需要多个子进程, 采用一个父进程+一个子进程的方式, 控制子进程的时间为2分钟(设置超时120s), 父进程等待子进程结束后再监听

技术要点

TCP: socket() - bind() - listen() - accept() - close()

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sock, connfd;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
exit(1);
}; // 创建套接字
SET_ADDR_PORT; // 考试要求, 简化sockaddr的填写
if (bind() < 0) exit(1); // 绑定
if (listen(sock, 5) < 0) exit(1); // 监听
for (;;) {
connfd = accept(); // 接收客户端连接
if (connfd < 0) exit(1);
... // 处理代码
wait(); // 等待子进程退出
close(connfd); // 关闭tcp连接
}

如何设置超时 - 利用信号

模板:

1
2
3
4
5
6
7
8
9
10
11
12
void sigalrm_handler(int sig) {
// 超时处理...
exit(1); // 这里超时子进程直接退出
}

int main () {
struct sigaction sigact;
sigact.sa_handler = sigalrm_handler; // 处理信号的函数
sigact.sa_mask = 0;
sigact.sa_flags = 0;
sigaction(SIGALRM, &sigact, NULL); // 绑定信号和
}

老师说直接signal(SIGALRM, sigalrm_handler)也可以

创建子进程

pid_t pid = fork()

pid == 0为子进程

pid > 0为父进程

完整代码

(老师的标程)

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
void sigalrm_handler(int sig) {
exit(0);
}

int main () {
int sock, connfd;
struct sigaction sigact;
sigact.sa_handler = sigalrm_handler;
sigact.sa_flags = 0;
sigact.sa_mask = 0;
sigaction(SIGALRM, &sigact, NULL);
if ((sock == socket(AF_INET, SOCK_STREAM, 0)) < 0) {
exit(1);
}
SET_ADDR_PORT;
if (bind() < 0) exit(1);
if (listen(sock, 5) < 0) exit(1);
for (;;) {
connfd = accept();
if (connfd < 0) exit(1);
if (fork() == 0) {
close(sock); // 子进程关闭监听socket
alarm(120);
CONTROL_CAMERA;
exit(0);
}
close(connfd); // 父进程关闭连接socket
wait(NULL);
}
close(sock);
exit(0);
}

题目2

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
// 以下为采用主进程+子进程的方式
int main () {
signal(SIG_CHLD, SIG_IGN);
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) exit(1);
SET_ADDR_PORT;
bind();
listen(listenfd, 5);
for (;;) {
int connfd = accept();
if (fork() == 0) { // 子进程负责接收
close(listenfd);
char buf[1024];
read(connfd, buf, 1024);
string outbuf(buf);
cout << outbuf << endl;
close(connfd);
exit(0);
}
string str;
cin >> str;
write(connfd, str, sizeof(str));
close(connfd);
close(listenfd);
exit(0);
}
}

// 信号驱动io的方式
int sock;
char buf[1024];
void sigio_handler(int sig) {
int n = recvfrom(sock, buf, 1024, 0, (struct sockaddr*) &addr, &addrlen);
if (n < 0) exit(1);
string outbuf(buf);
cout << outbuf << endl;
}

int main () {
signal(SIGIO, sigio_handler);
sock = socket(AF_INET, SOCK_DGRAM, 0);
SET_ADDR_PORT;
bind();
if (sock < 0) exit(1);
int on = 1;
fcntl(sock, F_SETOWN, getpid());
ioctl(sock, FIOASYNC, &on);
// 父进程发送
string msg;
cin >> msg;
sendto(sockfd, msg.c_str(), sizeof(msg.c_str()), 0, (struct sockaddr*) &addr1, sizeof(addr1));
fcntl()
}

考点

根据题目要求, 不允许读写阻塞的情况发生

选择TCP协议工作, 采用一个主进程 + 一个子进程, 主进程负责收, 子进程负责发

技术要点

前面提到过的不再写

僵尸进程 - 父进程捕获子进程的SIGCHLD信号

和上面处理超时的逻辑一致, 不过信号变为SIGCHLD

1
2
3
4
5
6
7
8
9
void sigchild_handler (int sig) {
wait(NULL);
}

struct sigactio sigact;
sigact.sa_handler = sigchild_handler;
sigact.sa_mask = 0;
sigact.sa_flags = 0;
sigaction(SIGCHLD, &sigact, NULL);

老师说直接signal(SIGCHLD, SIG_IGN)也可以

完整代码

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
void sigchild_handler (int sig) {
wait(NULL);
}

int main () {
int sock, connfd, sock2;
struct sockaddr_int addr;
string str;
struct sigaction sigact;
sigact.sa_hadnler = sigchild_handler;
sigact.sa_mask = 0;
sigact.sa_flags = 0;
sigaction(SIGCHLD, &sigact, NULL);
// 子进程 - 负责发送
if (fork() == 0) {
if ((sock2 = socket(AF_INET, SOCK_STREAM, 0)) < 0)
exit(1);
SET_ADDR_PORT;
if (connect () < 0) exit(1);
cin >> str;
write(sock2, str.c_str(), sizeof(str));
close(sock2);
}
// 父进程 - 负责接收
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
exit(1);
SET_ADDR_PORT;
if (bind() < 0) exit(1);
if (listen(sock, 5) < 0) exit(1);
for (;;) {
connfd = accept();
if (connfd < 0) exit(1);
char ch[1024];
int n = read(connfd, ch, 1024);
close(connfd);
string str(ch);
cout << str;
}
close(sock);
}

题目3

考点

UDP协议的使用, 同时控制10w个灯, TCP难以满足实时性, 故采用UDP+位运算的方式, 用10w位的01组合代表每个灯的亮灭情况, 每个灯根据自己的编号取出状态位并将自己打开/关闭

将10w位以文件的方式存放在磁盘上

技术要点

UDP的连接建立

将tcp中的SOCK_STREAM改为SOCK_DGRAM即可

sock = socket(AF_INET, SOCK_DGRAM, 0)

注意本题中UDP采用广播的方式, 故使用SET_ADDR_PORT_255

完整代码

1
2
3
4
5
6
7
8
9
10
11
int main () {
int sock;
struct sockaddr_in addr;
string str;
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
exit(1);
SET_ADDR_PORT_255;
int len = READ_FILE(char* buf);
int n = send(sock, buf, len);
close(sock);
}

题目4

ICMP…?

题目5

考点

由于匿名管道只能在父子进程之间通信

若要实现子进程之间的管道通信, 只能采取子进程发送给父进程, 父进程转发给另一个子进程的方式

技术要点

管道的建立和读写

管道创建:

1
2
int pipe1[2]; // 0读, 1写
if (pipe(pipe1) < 0) cout << "pipe error"; // 创建管道

在读写时, 如果需要读, 就关闭写端, 如果需要写, 就关闭读端

如果不关闭写端, 由于管道是阻塞的, 在read时会一直阻塞住, 如果写端已经关闭, 在管道为空时read会返回0

1
2
3
4
5
// pipe1用于读, pipe2用于写
// pipe3 & 4用不到, 都给关了
close(pipe1[1]);
close(pipe2[0]);
close(pipe3[0]); close(pipe3[1]); close(pipe4[0]); close(pipe4[1]);

完整代码

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
int main () {
// 创建四个管道, 管道1, 2用于子进程A和父进程的交互, 管道3, 4用于子进程B和父进程的交互
int pipe1[2], pipe2[2], pipe3[2], pipe4[2];
char psend1[] = "hello";
char psend2[] = "welcome";
char precv1[16];
char precv2[16];
pid_t pid;
if (pipe(pipe1) < 0 || pipe(pipe2) < 0 || pipe(pipe3) < 0 || pipe(pipe4) < 0) {
cout << "pipe error" << endl;
exit(1);
}
pid = fork();
if (pid < 0) exit(1);
// child A, use pipe1 & 2
else if (pid == 0) {
cout << "child A" << endl;
close(pipe1[1]); // pipe1读, 关闭写端
close(pipe2[0]); // pipe2写, 关闭读端
close(pipe3[0]); close(pipe3[1]); close(pipe4[0]); close(pipe4[1]);
for (;;) {
if (read(pipe1[0], precv1, 16) > 0) {
cout << "recv : " << precv1 << endl;
}
write(pipe2[1], psend1, sizeof(psend1));
}
exit(0);
}
pid = fork();
if (pid < 0) exit(1);
// child B, use pipe3 & 4
else if (pid == 0) {
cout << "child B" << endl;
close(pipe3[1]);
close(pipe4[0]);
close(pipe1[0]); close(pipe1[1]); close(pipe2[0]); close(pipe2[1]);
for (;;) {
write(pipe4[1], psend2, sizeof(psend2));
if (read(pipe3[0], precv2, 16) > 0) {
cout << "recv : " << precv2 << endl;
}
}
exit(0);
}
// 父进程
close(pipe1[0]);
close(pipe2[1]);
close(pipe3[0]);
close(pipe4[1]);
for (;;) {
read(pipe4[0], precv1, 16);
write(pipe1[1],precv1, sizeof(precv1));
read(pipe2[0], precv2, 16);
write(pipe3[1], precv2, sizeof(precv2));
}
}

题目6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 考虑到只有1000个进程, 故无法使用并发, 可以考虑使用非阻塞io
int main () {
const int N = 20000;
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) exit(1);
SET_ADDR_PORT;
if (bind() < 0) exit(1);
if (listen(sockfd, 5) < 0) exit(1);
int connfd[N];
for (int i = 0; i < N; ++i) {
connfd[i] = accept();
if (connfd[i] < 0) exit(1);
int flag = fcntl(connfd[i], F_GETFL, 0);
fcntl(connfd[i], F_SETFL, flag | O_NONBLOCK); // 设置非阻塞
}
for (;;) {
sleep(120);
for (int i = 0; i < N; ++i) {
if (write(connfd[i], msg, sizeof(msg)) < 0) exit(1);
}
}
}

考点

「长连接」-> TCP

非阻塞IO模型

由于只能创建2000个子进程, 故不能使用并发服务, 这里采用非阻塞io的方式, 先接受20000个客户端的连接, 然后每隔2分钟(120s)向所有客户机发送一组数据

技术要点

设置socket为非阻塞

第一种方式: 使用fcntl

先拿到flag, 再加上非阻塞的条件

1
2
int flag = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flag | O_NONBLOCK);

第二种方式: 使用ioctl

1
2
int on = 1;
ioctl(sock, FIONBIO, &on);

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main () {
int sock, connfd[2000];
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
exit(1);
SET_ADDR_PORT;
if (bind() < 0) exit(1);
if (listen(sock, 5) < 0) exit(1);
int num = 0;
while (num < 20000) {
connfd[num] = accept();
if (connfd[num] < 0) exit(1);
int flag = fcntl(connfd[num], F_GETFL, 0);
fcntl(connfd[num], F_SETFL, flag | O_NONBLOCK);
++num;
}
while (1) {
for (int i = 0; i < 20000; ++i) {
int n = write(connfd[i], str.c_str(), 6);
}
sleep(120);
}
close(sock);
exit(0);
}

2 | 2020年题目

题目1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main () {
// 外网通信
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) exit(1);
SET_PORT_ADDR;
if (bind() < 0) exit(1);
if (listen(sockfd, 5) < 0) exit(1);
int connfd = accpet();
if (connfd < 0) exit(1);
char buf[1024];
// 内网通信
int insock = socket(AF_INET, SOCK_DGRAM, 0);
if (insock < 0) exit(1);
SET_PORT_ADDR_255; // 广播
bind();
for (;;) {
int n = read(connfd, buf, 1024);
if (n < 0) exit(1);
// 转发到内网
n = sendto(insock, buf, sizeof(buf), ...);
if (n < 0) exit(1);
}
}

考点

该程序分为:

  • 外网通信
  • 内网通信

两个部分, 其中外网通信部分采用单播, 而内网通信采用广播, 故外网通信部分使用TCP通信而内网通信部分使用UDP通信

完整代码

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
int main () {
int sock, connfd;
struct sockaddr_in addr, addrX;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
exit(1);
}
SET_ADDR_PORT;
if (bind(sock) < 0) exit(1);
if (listen(sock, 5) < 0) exit(1);
char buf[1024];
for (;;) {
// 外网通信 - TCP
connfd = accpet(sock);
if (connfd < 0) exit(1);
int n = read(connfd, buf, 1024);
if (n < 0) exit(1);
// 内网通信 - UDP
int socku = socket(AF_INET, SOCK_DGRAM, 0);
if (socku < 0) exit(1);
SET_ADDR_PORT_255;
n = sendto();
close(socku);
close(connfd);
exit(0);
}
}

题目2

考点

对于整数的乘除法, 需要考虑主机字节序和网络字节序的转换问题, 设计实现为: 每接收一个客户机创建一个子进程服务客户机的计算

规定接收数据的格式如下:

int mType, int n1, int n2

其中mType表示计算方式(0-乘法, 1-除法)

n1, n2为两个操作数

规定发送数据的格式如下:

int mRet, int n

其中mRet = 0表示计算成功, 1表示发生错误, n为运算结果

(程序中省略了发送和接收报文后对数据提取的处理, 默认read后mType, n1, n2已被读取, write后mRet, n已被发送)

技术要点

僵尸进程的清除

楼上有说, 采用捕获SIGCHLD信号的方式(标程采用)

1
2
3
4
5
6
7
8
9
10
11
void sigchild_handler(int sig) {
wait(NULL);
}

int main () {
struct sigaction sigact;
sigact.sa_handler = sigchild_handler;
sigact.sa_flags = 0;
sigact.sa_mask = 0;
sigaction(SIGCHLD, &sigact, NULL);
}

另外, 我感觉也可以直接signal(SIGCHLD, SIG_IGN);(未得到老师确定)

字节序转换问题

由于试卷上已经说明int->char可用IntToChar代替, char->int可用CharToInt代替, 故数据在网络中以字符串形式传输, 使用ntohlhtonl进行字节序的转换

发送时

1
2
3
int ans;
ans = htonl(ans);
IntToChar(ans);

接收时

1
2
read(connfd, buf, 1024)
int ans = ntohl(CharToInt(buf));

完整程序

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
void sigchild_handler (int sig) {
wait(NULL);
}

int main () {
int sock, connfd;
struct sockaddr_n addr;
struct sigaction sigact;
sigact.sa_handler = sigchild_handler;
sigact.sa_flags = 0;
sigact.sa_mask = 0;
sigaction(SIGCHLD, &sigact, NULL);

int n, n1, n2;
int mRet, mType;
char buf[1024];

if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
exit(1);
SET_ADDR_PORT;
if (bind() < 0) exit(1);
if (listen(sock, 5) < 5) exit(1);

for (;;) {
connfd = accept();
if (connfd < 0) exit(1);
if (fork == 0) {
close(sock);
int n = read(connfd, buf, 1024);
if (n < 0) exit(1);
mRet = 0;
CharToInt;
ntoh(); // 省略的写法, 实际上这里处理了n1和n2
if (mType == 0) {
n = hton(n1 * n2);
} else if (mType == 1 && n2 != 0) {
n = hton(n1 / n2);
} else {
mRet = hton(1);
n = 0;
}
IntToChar;
write(connfd, buf, 2 * sizeof(int));
close(connfd);
exit(0);
}
close(connfd);
}
close(sock);
exit(0);
}

题目3

考点

客户机数据格式设计为最前面的一个字节表示A或B的ID号, 标记为IDA和IDB, 其后是发送给对方的数据, AB都和C进行连接, 在C上使用非阻塞式的I/O模型的TCP服务器, C在与A,B都保持连接的情况下, 收到一组数据后就转发

非阻塞IO上面写过了这里就不写了, 感觉这题直接用select也可以…

完整程序

非阻塞IO

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
int main () {
int sock, sockA, sockB;
int n, ida = 0, idb = 0; // 两个连接的标记
struct sockaddr_in addr;
char a;
char buf[1024];

if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
exit(1);
}
SET_ADDR_PORT;
if (bind() < 0) exit(1);
if (listen(sock, 5) < 0) exit(1);
while (ida == 0 || idb == 0) {
connfd = accept();
if (connfd < 0) exit(1);
n = read(connfd, buf, 1);
a = buf[0];
if (a == IDA) {
sockA = connfd;
} else if (a == IDB) {
sockB = connfd;
}
}
// 设置为非阻塞
int flag = fcntl(sockA, F_GETFL, 0);
fcnt(sockA, F_SETFL, flag | O_NONBLOCK);
int flag = fcntl(sockB, F_GETFL, 0);
fcnt(sockB, F_SETFL, flag | O_NONBLOCK);
for (;;) {
n = read(sockA, buf, 1024);
if (n > 0) {
write(sockB, buf, n);
}
n = read(sockB, buf, 1024);
if (n > 0) {
write(sockA, buf, n);
}
}
close(sockA); close(sockB); close(sock);
}

题目4

考点

信号驱动I/O模式的编程, 如何实现数据同步.

数据同步即需要做到在数据采集的过程中, 不会被用户的请求所打扰, 这里采用一个OK标记和复制的方式保护数据的完整性

技术要点

信号驱动方式的设置:

1
2
3
4
5
6
7
8
9
10
void sigio_handler(int sig) {
//...
}

int main () {
signal(SIGIO, sigio_handler); // 信号-信号处理函数
fcntl(sockfd, F_SETOWN, getpid()); // 设置宿主
int on = 1;
ioctl(sockfd, FIOAYNC, &on); // 异步开启
}

注意读写的主机-网络字节序转换

完整程序

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
char buf[256];
int photo1[4096], photo2[4096];
int nOK[2];
struct sockaddr_in addr;
int sock;

void sigio_handler(int sig) {
int tmp[4096];
int n = recvfrom();
if (n <= 0) exit(1);
if (nOK[0] == 1) {
for (int j = 0; j < 4096; ++j) {
tmp[j] = htonl(photo1[j]);
}
} else if (nOK[1] == 1) {
for (int j = 0; j < 4096; ++j) {
tmp[j] = htonl(photo2[j]);
}
} else { // 全填充0
for (int j = 0; j < 4096; ++j) {
tmp[j] = htonl(0);
}
}
sendto();
}

int main () {
struct sigaction sigact;
sigact.sa_handler = sigio_handler;
sigact.sa_mask = 0;
sigact.sa_flags = 0;
sigaction(SIGIO, &sigact, NULL);
nOK[0] = nOK[1] = 0;
memset(photo1, 0, sizeof(photo1));
memset(photo2, 0, sizeof(photo2));
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) exit(1);
SET_ADDR_PORT;
if (bind() < 0) exit(1);
if (listen(sock, 5) < 0) exit(1);
// 信号驱动IO
fcntl(sock, F_SETOWN, getpid());
int on = 1;
ioctl(sock, FIOASYNC, &on);
int tp[4096];
for (;;) {
takePhoto(tp);
nOK[0] = 0;
for (int i = 0; i < 4096; ++i) {
photo1[i] = tp[i];
}
nOK[0] = 1;

nOK[1] = 0;
for (int i = 0; i < 4096; ++i) {
photo2[i] = tp[i];
}
nOK[1] = 1;
}
close(sock);
}

其他

守护进程

守护进程的创建过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
void daemon_init() {
if (fork() != 0) exit(0); // 首先父进程退出
if (setsid() < 0) exit(0); // 然后setsid()
signal(SIGHUP, SIG_IGN); // 忽略SIGHUP信号
if (fork() != 0) exit(0); // 再次退出
chdir("/"); umask(0); // 不适用任何目录, 对任何写内容有权限
for (int i = 0; i < NOFILE; ++i) { // 关闭所有文件描述符
close(i);
}
signal(SIG_CHLD, SIG_IGN); // 避免僵尸进程
return 0;
}

有名管道

命名管道使用

  • 写进程使用mkfifo创建命名管道
  • 写进程调用open以写阻塞方式打开管道
  • 读进程调用open以读阻塞方式打开管道
  • 写进程调用write写入数据
  • 读进程调用read读出数据

有名管道的创建

1
2
#define FIFO_NAME "/xxx/xxx"
mkfifo(FIFO_NAME, O_CREAT|O_EXCL)

有名管道阻塞写打开

1
int fd = open(FIFO_NAME, O_WRONLY, 0);

有名管道阻塞读打开

1
int fd = open(FIFO_NAME, O_RDONLY, 0);

地址可重用

1
2
3
4
int listenfd;
int on = 1;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int));

IO多路复用

多路复用

1
2
3
4
5
6
7
int select(int maxfd,  struct fd_set* rdset,  struct fd_set* wrset, 
struct fd_set* exset, struct timeval* timeout);

void FD_SET(int fd,fd_set *fdset) //将fd加入到fdset
void FD_CLR(int fd,fd_set *fdset) //将fd从fdset里面清除
void FD_ZERO(fd_set *fdset) //从fdset中清除所有的文件描述符
int FD_ISSET(int fd,fd_set *fdset) //判断fd是否在fdset集合中

select( )可以设置超时,使长期没有文件描述符就绪时,进程可以跳出阻塞状态。select( )的第一个参数 maxfd 是集合中最大的文件描述符加1,如:一个包含3个套接字描述符的集合{12,23,30},那么 maxfd 就应该是30+1=31。

在我们调用select( )时,进程会一直阻塞到以下的一种情况发生:

  • 有文件可以读,包括出现错误;
  • 有文件可以写,包括出现错误;
  • 超时所设置的时间到;
  • 被信号中断。

下面例子中,客户机与多个服务器建立连接,然后设置读描述符集合,调用函数将进程阻塞。

当任一个服务器的数据到达时,进程就被唤醒,检查集合中哪个描述符就绪,然后作相应处理。

代码片段如下:

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
int main(int argc,char*argv[])
{
int sockfd[NUMBER]; /* NUMBER--需要建立的套接字数量 */
struct sockaddr_in servaddr[NUMBER];
fd_set rfds;
char buf[1024];
int i;

for(i=0;i<NUMBER;i++)
{
sockfd[i]=socket(AF_INET,SOCK_STREAM,0);
if(sockfd[i]<0)
exit(1);
}
....../*填充NUMBER个地址结构*/
....../*建立NUMBER个连接*/
int nOK[NUMBER];
for(i=0;i<NUMBER;i++) {nOK[i]=0;}
int nEnd=NUMBER;
while(nEnd!=0)
{
for(i=0;i<NUMBER;i++)
if(nOK[i]==0) /*只检查未被处理的套接字*/
FD_SET(sockfd[i],&rds);

n=select(theMax(NUMBER,sockfd)+1,&rds,NULL,,NULL,,NULL);
if(n<0 && errno==EINTR)
continue;

for(i=0;i<NUMBER;i++)
{
if( FD_ISSET(sockfd[i],&rds) )
{
n=read(sockfd[i],buf,1024);
if(n<=0 && errno!=EINTR)
{
perror("An Error."); /*发生错误,这个套接字不再工作*/
nOK[i]=1;nEnd--;
}
else if(n>0)
{
process(buf,...);/*数据处理*/
nOK[i]=1;nEnd--;
}
}
}
}
for(i=0;i<NUMBER;i++) {close(sockfd[i]);}
}

「网络程序设计」期末复习
https://blog.roccoshi.top/posts/10502/
作者
RoccoShi
发布于
2021年6月11日
许可协议