注:博主只关注编程实现的方面以及linux部分,部分网络原理讲解和windows实现部分跳过
第 6 章 基于 UDP 的服务端/客户端
6.1 理解 UDP
- Q:UDP原理和使用
- UDP比TCP简单,编程难度较小,性能比TCP高。TCP相对UDP的主要区别是流控制
- IP 的作用就是让离开主机 B 的 UDP 数据包准确传递到主机 A 。但是把 UDP 数据包最终交给主机 A 的某一 UDP 套接字的过程是由 UDP 完成的。UDP 的最重要的作用就是根据端口号将传到主机的数据包交付给最终的 UDP 套接字
- UDP 也具有一定的可靠性。在网络实时传递的视频或者音频的场景下,丢失一部分多媒体数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。此时TCP流控制就显得有一点多余,这时就要考虑使用 UDP
- TCP 比 UDP 慢的原因主要有以下两点:
- 收发数据前后进行的连接设置及清除过程
- 收发过程中为保证可靠性而添加的流控制
- 如果收发的数据量小但是需要频繁连接时,UDP 比 TCP 更高效
6.2 实现基于 UDP 的服务端/客户端
- Q:基于 UDP 的服务端/客户端的特点
- UDP 的服务端和客户端无需经过连接过程,只有创建套接字和数据交换的过程
- UDP 的服务端和客户端均只需1个套接字
- Q:基于 UDP 的数据 I/O 函数
- UDP 套接字不会保持连接状态,因此每次传输数据时都需要添加目标的地址信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ssize_t sendto(int sock,
void * buff,
size_t nbytes,
int flags,
struct sockaddr * to,
socklen_t addrlen);
// sock:用于传输数据的UDP套接字文件描述符
// buff:保存待传输数据的缓冲地址值
// nbytes:待传输的数据长度,以字节为单位
// flags:可选参数,若没有则传递0
// to:存有目标地址信息的sockaddr结构体变量的地址值
// addrlen:传递给参数to的地址值结构体变量长度
// 成功时返回传输的字节数,失败时返回 -1 - UDP 数据的发送端并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ssize_t recvfrom(int sock,
void * buff,
size_t nbytes,
int flags,
struct sockaddr * from,
socklen_t * addrlen);
// sock:用于传输数据的UDP套接字文件描述符
// buff:保存接收数据的缓冲地址值
// nbytes:可接收的最大字节数,故无法超过参数buff所指的缓冲大小
// flags:可选项参数,若没有则传递0
// from:存有发送端地址信息的sockaddr结构体变量的地址值
// addrlen:保存参数from的结构体变量长度的变量地址值
// 成功时返回接收的字节数,失败时返回 -1
- UDP 套接字不会保持连接状态,因此每次传输数据时都需要添加目标的地址信息
- Q:基于 UDP 的服务端/客户端的程序实现
- 服务端uecho_server.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char * argv[])
{
int serv_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 创建 UDP 套接字后,向 socket 的第二个参数传递 SOCK_DGRAM
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1) {
error_handling("UDP socket creation error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
// 分配地址接受数据,不限制数据传输对象
if (bind(serv_sock,
(struct sockaddr *)&serv_adr,
sizeof(serv_adr)) == -1) {
error_handling("bind() error");
}
while (1) {
clnt_adr_sz = sizeof(clnt_adr);
str_len = recvfrom(serv_sock, message,
BUF_SIZE, 0,
(struct sockaddr *)&clnt_adr,
&clnt_adr_sz);
// 通过上面的函数调用同时获取数据传输端的地址。
// 正是利用该地址进行逆向重传
sendto(serv_sock, message, str_len, 0,
(struct sockaddr *)&clnt_adr, clnt_adr_sz);
}
// 上面未加入break语句,因此无限循环,下面不会执行
close(serv_sock);
return 0;
} - 客户端uecho_client.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char * argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
while (1) {
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
break;
}
sendto(sock, message, strlen(message), 0,
(struct sockaddr *)&serv_adr, sizeof(serv_adr));
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr *)&from_adr, &adr_sz);
message[str_len] = '\0';
printf("Message from server: %s", message);
}
close(sock);
return 0;
} - 编译运行
- 服务端
1
2shiqi@inspiron:~/network$ gcc uecho_server.c -o userver
shiqi@inspiron:~/network$ ./userver 9190 - 客户端
1
2
3
4
5shiqi@inspiron:~/network$ gcc uecho_client.c -o uclient
shiqi@inspiron:~/network$ ./uclient 127.0.0.1 9190
Insert message(q to quit): xp
Message from server: xp
Insert message(q to quit): q
- 服务端
- 服务端uecho_server.c
- Q:UDP 客户端套接字的地址分配
- 在首次调用sendto函数时,发现尚未分配地址信息,则给相应套接字自动分配 IP 和端口,此时分配的地址一直保留到程序结束为止
6.3 UDP 的数据传输特性和调用 connect 函数
- Q:存在数据边界的UDP套接字程序示例
- bound_host1.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char * argv[])
{
int sock;
char message[BUF_SIZE];
struct sockaddr_in my_adr, your_adr;
socklen_t adr_sz;
int str_len, i;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
error_handling("socket() error");
}
memset(&my_adr, 0, sizeof(my_adr));
my_adr.sin_family = AF_INET;
my_adr.sin_addr.s_addr = htonl(INADDR_ANY);
my_adr.sin_port = htons(atoi(argv[1]));
if (bind(sock, (struct sockaddr *)&my_adr, sizeof(my_adr)) == -1) {
error_handling("bind() error");
}
for (i = 0; i < 3; ++i) {
sleep(5);
adr_sz = sizeof(your_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr *)&your_adr, &adr_sz);
printf("Message %d: %s \n", i + 1, message);
}
return 0;
} - bound_host2.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char * argv[])
{
int sock;
char msg1[] = "Hi!";
char msg2[] = "I'm another UDP host!";
char msg3[] = "Nice to meet you";
struct sockaddr_in your_adr;
socklen_t your_adr_sz;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
error_handling("socket() error");
}
memset(&your_adr, 0, sizeof(your_adr));
your_adr.sin_family = AF_INET;
your_adr.sin_addr.s_addr = inet_addr(argv[1]);
your_adr.sin_port = htons(atoi(argv[2]));
sendto(sock, msg1, sizeof(msg1), 0,
(struct sockaddr *)&your_adr, sizeof(your_adr));
sendto(sock, msg2, sizeof(msg2), 0,
(struct sockaddr *)&your_adr, sizeof(your_adr));
sendto(sock, msg2, sizeof(msg2), 0,
(struct sockaddr *)&your_adr, sizeof(your_adr));
close(sock);
return 0;
} - 编译运行
- bound_host1
1
2
3
4
5shiqi@inspiron:~/network$ gcc bound_host1.c -o host1
shiqi@inspiron:~/network$ ./host1 9190
Message 1: Hi!
Message 2: I'm another UDP host!
Message 3: I'm another UDP host! - bound_host2
1
2shiqi@inspiron:~/network$ gcc bound_host2.c -o host2
shiqi@inspiron:~/network$ ./host2 127.0.0.1 9190
- bound_host1
- bound_host1.c
- Q:sendto 函数传输数据的过程的 3 个阶段
- 向 UDP 套接字注册目标 IP 和端口号
- 传输数据
- 删除 UDP 套接字中注册的目标地址信息
- Q:未连接UDP套接字和已连接UDP套接字
- 未连接UDP套接字:未注册目标地址信息的套接字,每次都变更目标地址,可重复利用同一 UDP 套接字向不同目标传递数据
- 已连接UDP套接字:注册了目标地址的套接字,要与同一主机长时间通信时,将UDP套接字变成已连接套接字会提高效率
- Q:针对UDP套接字调用connect函数创建已连接UDP套接字
1
2
3
4
5
6sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = ...
adr.sin_port = ...
connect(sock, (struct sockaddr *)&adr, sizeof(adr));- 因已经指定了收发对象,所以不仅可以使用 sendto、recvfrom 函数,还可以使用 write、read 函数进行通信
- Q:已连接UDP套接字程序示例
- 服务端同uecho_server.c
- 客户端uecho_con_client.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char * argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
while (1) {
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
break;
}
write(sock, message, strlen(message));
str_len = read(sock, message, sizeof(message) - 1);
message[str_len] = '\0';
printf("Message from server: %s", message);
}
close(sock);
return 0;
} - 编译运行同echo的udp服务器
第 7 章 优雅的断开套接字的连接
7.1 基于 TCP 的半关闭
- Q:单方面断开连接带来的问题
- Linux 的 close 函数意味着完全断开连接。即不仅无法传输数据,而且也不能接收数据。因此在某些情况下,通信一方单方面的断开套接字连接,显得不太优雅
- 断开一部分连接,即可以传输数据但是无法接收,或可以接受数据但无法传输。即关闭流的一半
- Q:针对优雅断开的 shutdown 函数原型
1
2
3
4
5
int shutdown(int sock, int howto);
// sock:需要断开的套接字文件描述符
// howto:传递断开方式信息
// 成功时返回 0 ,失败时返回 -1- 第二个参数决定断开连接的方式
- SHUT_RD:断开输入流
- SHUT_WR:断开输出流
- SHUT_RDWR:同时断开I/O流
- 第二个参数决定断开连接的方式
- Q:基于半关闭的文件传输程序
- 服务端file_server.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char * argv[])
{
int serv_sd, clnt_sd;
FILE * fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
fp = fopen("file_server.c", "rb");
serv_sd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
bind(serv_sd,
(struct sockaddr *)&serv_adr,
sizeof(serv_adr));
listen(serv_sd, 5);
clnt_adr_sz = sizeof(clnt_adr);
clnt_sd = accept(serv_sd,
(struct sockaddr *)&clnt_adr,
&clnt_adr_sz);
while (1) {
read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
if (read_cnt < BUF_SIZE) {
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
shutdown(clnt_sd, SHUT_WR);
read(clnt_sd, buf, BUF_SIZE);
printf("Message from client: %s \n", buf);
fclose(fp);
close(clnt_sd);
close(serv_sd);
return 0;
} - 客户端file_client.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char * argv[])
{
int sd;
FILE *fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
fp = fopen("receive.dat", "wb");
sd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
connect(sd,
(struct sockaddr *)&serv_adr,
sizeof(serv_adr));
while ((read_cnt = read(sd, buf, BUF_SIZE)) != 0) {
fwrite((void *)buf, 1, read_cnt, fp);
}
puts("Received file data");
write(sd, "Thank you", 10);
fclose(fp);
close(sd);
return 0;
} - 编译运行
- 服务端
1
2
3shiqi@inspiron:~/network$ gcc file_server.c -o fserver
shiqi@inspiron:~/network$ ./fserver 9190
Message from client: Thank you - 客户端
1
2
3shiqi@inspiron:~/network$ gcc file_client.c -o fclient
shiqi@inspiron:~/network$ ./fclient 127.0.0.1 9190
Received file data
- 服务端
- 服务端file_server.c
第 8 章 域名及网络地址
8.1 域名系统
8.2 IP地址和域名之间的转换
- Q:利用域名获取IP地址的函数gethostbyname
1
2
3
struct hostent * gethostbyname(const char * hostname);
// 成功时返回 hostent 结构体地址,失败时返回 NULL 指针- 只要传递字符串,就可以返回域名对应的IP地址。返回时地址信息装入 hostent 结构体
1
2
3
4
5
6
7struct hostent {
char * h_name; // official name
char ** h_aliases; // alias list
int h_addrtype; // host address type
int h_length; // address length
char ** h_addr_list; // address list
} - h_name:该变量中存有官方域名(Official domain name)。即代表某一主页,但实际上,一些著名公司的域名并没有用官方域名注册
- h_aliases:可通过多个域名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可以指定其他域名。这些信息可以通过 h_aliases 获得
- h_addrtype:gethostbyname 函数不仅支持 IPV4 还支持 IPV6 。因此可以通过此变量获取保存在 h_addr_list 的IP地址族信息。若是 IPV4 ,则此变量中存有 AF_INET
- h_length:保存IP地址长度。若是 IPV4 地址,因为是 4 个字节,则保存4;IPV6 时,因为是 16 个字节,故保存 16
- h_addr_list:通过此变量以整数形式保存域名对应的IP地址。另外,用户较多的网站有可能分配多个IP给同一域名,利用多个服务器进行负载均衡。此时同样可通过此变量获取IP地址信息,注意,字符串指针数组中的元素实际指向的是 in_addr 结构体变量地址值而非字符串,所以需要进行类型转换,并调用 inet_ntoa 函数
- 调用 gethostbyname 函数后,返回的结构体变量如图所示
- 只要传递字符串,就可以返回域名对应的IP地址。返回时地址信息装入 hostent 结构体
- Q:gethostbyname函数示例gethostbyname.c
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
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int i;
struct hostent * host;
if (argc != 2) {
printf("Usage : %s <addr>\n", argv[0]);
exit(1);
}
host = gethostbyname(argv[1]);
if (!host) {
error_handling("gethost... error");
}
// 输出官方域名
printf("Official name: %s \n", host->h_name);
// 输出除官方域名以外的域名
for (i = 0; host->h_aliases[i]; ++i) {
printf("Aliases %d: %s \n", i+1, host->h_aliases[i]);
}
// 看看是不是ipv4
printf("Address type: %s \n",
(host->h_addrtype == AF_INET)? "AF_INET": "AF_INET6");
// 输出ip地址信息
for (i = 0; host->h_addr_list[i]; ++i) {
printf("IP addr %d: %s \n", i+1,
inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
}
return 0;
}- 编译运行
1
2
3
4
5
6
7
8
9
10
11
12
13shiqi@inspiron:~/network$ gcc gethostbyname.c -o hostname
shiqi@inspiron:~/network$ ./hostname www.naver.com
Official name: www.naver.com.gccdn.net
Aliases 1: www.naver.com
Aliases 2: www.naver.com.nheos.com
Address type: AF_INET
IP addr 1: 43.243.234.60
shiqi@inspiron:~/network$ ./hostname www.baidu.com
Official name: www.a.shifen.com
Aliases 1: www.baidu.com
Address type: AF_INET
IP addr 1: 220.181.38.149
IP addr 2: 220.181.38.150
- 编译运行
- Q:利用IP地址获取域名gethostbyaddr函数
1
2
3
4
5
6
7
8
9
10
struct hostent * gethostbyaddr(const char * addr,
socklen_t len,
int family);
// addr: 含有IP地址信息的 in_addr 结构体指针。
// 为了同时传递IPv4地址之外的其它信息,
// 该变量的类型声明为char指针
// len: 向第一个参数传递的地址信息的字节数,IPV4 时为 4,IPV6 时为 16
// family: 传递地址族信息,ipv4 是 AF_INET ,IPV6是 AF_INET6
// 成功时返回 hostent 结构体变量地址值,失败时返回 NULL 指针 - Q:gethostbyaddr函数示例gethostbyaddr.c
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 error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int i;
struct hostent *host;
struct sockaddr_in addr;
if (argc != 2) {
printf("Usage : %s <IP>\n", argv[0]);
exit(1);
}
memset(&addr, 0, sizeof(addr));
addr.sin_addr.s_addr = inet_addr(argv[1]);
host = gethostbyaddr((char *)&addr.sin_addr, 4, AF_INET);
if (!host) {
error_handling("gethost... error");
}
// 输出官方域名
printf("Official name: %s \n", host->h_name);
// 输出除官方域名以外的域名
for (i = 0; host->h_aliases[i]; ++i) {
printf("Aliases %d: %s \n", i+1, host->h_aliases[i]);
}
// 看看是不是ipv4
printf("Address type: %s \n",
(host->h_addrtype == AF_INET)? "AF_INET": "AF_INET6");
// 输出ip地址信息
for (i = 0; host->h_addr_list[i]; ++i) {
printf("IP addr %d: %s \n", i+1,
inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
}
return 0;
}- 编译运行
1
2
3
4
5
6
7
8
9shiqi@inspiron:~/network$ gcc gethostbyaddr.c -o hostaddr
shiqi@inspiron:~/network$ ./hostaddr 8.8.8.8
Official name: dns.google
Address type: AF_INET
IP addr 1: 8.8.8.8
shiqi@inspiron:~/network$ ./hostaddr 114.114.114.114
Official name: public1.114dns.com
Address type: AF_INET
IP addr 1: 114.114.114.114
- 编译运行