计网实验——Socket编程

实验目的

  • 掌握TCP和UDP协议主要特点和工作原理
  • 理解socket的基本概念和工作原理
  • 编程实现socket网络通信(C++ & Python)

实验内容与分析

实验环境

CentOS 7.7 + Python3.6.8

任务1:实现字符串逆序回送

任务要求:

image-20200322211802446

Solution ①:

(1) 客户机的源代码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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <error.h>
#include <stdlib.h>
/* 不接收换行符的fgets()函数 */
void my_fgets(char data[], int count)
{
fgets(data, count, stdin);
/* 找出data中的"\n" */
char *find = strchr(data, '\n');
/* 截取换行符前的字符串 */
if(find)
*find = '\0';
}
int main(int argc, char *argv[])
{
int client_sock;
errno = 0;
struct sockaddr_in server_addr;
char send_msg[255];
char recv_msg[255];
char stop_word[]="bye";
int is_esc;
int i;

/* 创建socket */
/*************************************************
函数原型:int socket(int family, int type, int protocol);
参数:
family:协议族。实验用AF_INET
type:socket类型。常见有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等
protocol:具体协议。0表示默认协议
返回值:
成功:返回一个新建的socket,用于数据传输
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
client_sock = socket(AF_INET, SOCK_STREAM, 0);
if (errno)
{
perror("创建socket失败");
return 0;
}

/* 指定服务器地址 */

/* 用于socket创建通信连接的类型,这里就是ipv4地址类型的通信连接可用 */
server_addr.sin_family = AF_INET;
/* 整型变量从主机字节顺序转变成网络字节顺序:Big-Endian */
server_addr.sin_port = htons(atoi(argv[2]));
/* 将点分十进制的IP地址转化为无符号长整数型数的网络字节序 */
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
/* 进行零填充 */
memset(server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));
if (errno)
{
perror("指定服务器地址失败");
return 0;
}

/* 连接服务器 */

/*************************************************
函数原型: int connect(int sockfd, const struct sockaddr *addr, int addrlen);
参数:
sockfd:用于发起连接的socket的描述符
addr:服务器地址。此处为指向通用地址结构的指针,使用时,要进行强制类型转换
addrlen:服务器地址结构大小。可使用sizeof自动计算
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
connect(client_sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (errno)
{
perror("连接服务器失败");
return 0;
}

while(1)
{
/* 发送消息 */

is_esc=0;
printf("Myself: ");
my_fgets(send_msg, 256);
/*************************************************
函数原型: int send(int sockfd, const void *buf, int len, int flags);
参数:
sockfd:用于发送数据的socket的描述符
buf:指向数据的指针
len:发送数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回发送的数据大小
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
send(client_sock, send_msg, strlen(send_msg), 0);
if (errno)
{
perror("发送消息失败");
return 0;
}

for(i = 0; i < strlen(send_msg); i++)
{
if(send_msg[i] == 27)
{
is_esc = 1;
break;
}
}
/* 如果发送消息是bye或者含有ESC就退出 */
if (strcmp(send_msg,stop_word) == 0 || is_esc )
{
printf("\n");
break;
}

/* 接收并显示消息 */

/* 接收数组置零 */
memset(recv_msg, 0, sizeof(recv_msg));

/*************************************************
函数原型: int recv(int sockfd, void *buf, int len, int flags);
参数:
sockfd:用于接收数据的socket的描述符
buf:指向数据的指针
len:接收数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回接收的数据大小
失败:如果对方已关闭连接,返回0;其他错误返回-1,并在全局变量errno中记录错误类型
*************************************************/
if (recv(client_sock, recv_msg, sizeof(recv_msg), 0) == 0)
{
printf("Connection interrupted\n");
break;
}
printf("Server: %s\n", recv_msg);
if (errno)
{
perror("发送消息失败");
return 0;
}
}



/* 关闭socket */

/*************************************************
函数原型: int close(int sockfd);
参数:
sockfd:要关闭的socket的描述符
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
close(client_sock);
if (errno)
{
perror("关闭socket失败");
return 0;
}

return 0;
}

(2) 服务器的源代码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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
/* 字符串逆序函数 */
char *strrev(char *str)
{
char *p1, *p2;

if (! str || ! *str)
return str;
for (p1 = str, p2 = str + strlen(str) - 1; p2 > p1; ++p1, --p2)
{
*p1 ^= *p2;
*p2 ^= *p1;
*p1 ^= *p2;
}
/* UTF-8汉字一个是三个字节,进行相应逆序转化 */
for (p2 = str + strlen(str) - 1; p2 >= str; --p2)
{
if (*p2&0x80)//最高位为1
{

*p2 ^= *(p2-2);
*(p2-2) ^= *p2;
*p2 ^= *(p2-2);
p2 = p2-2;
}
}
return str;
}
int main(int argc, char *argv[])
{
int server_sock_listen, server_sock_data;
struct sockaddr_in server_addr;
char recv_msg[255];
char stop_word[]="bye";
int is_esc;
int i;

/* 创建socket */

/*************************************************
函数原型: int socket(int family, int type, int protocol);
参数:
family:协议族。实验用AF_INET
type:socket类型。常见有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等
protocol:具体协议。0表示默认协议
返回值:
成功:返回一个新建的socket,用于数据传输
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
server_sock_listen = socket(AF_INET, SOCK_STREAM, 0);
if (errno)
{
perror("创建socket失败");
return 0;
}

/* 指定服务器地址 */

/* 用于socket创建通信连接的类型,这里就是ipv4地址类型的通信连接可用 */
server_addr.sin_family = AF_INET;
/* 整型变量从主机字节顺序转变成网络字节顺序:Big-Endian */
server_addr.sin_port = htons(atoi(argv[1]));
/* INADDR_ANY表示本机所有IP地址 */
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/* 进行零填充 */
memset(&server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));
if (errno)
{
perror("指定服务器地址失败");
return 0;
}

/* 绑定socket与地址 */

/*************************************************
函数原型: int bind(int sockfd, const struct sockaddr *addr, int addrlen);
参数:
sockfd:要绑定的socket的描述符
addr:要绑定的地址。此处为指向通用地址结构的指针,使用时,要进行强制类型转换
addrlen:地址结构大小。可使用sizeof自动计算
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
bind(server_sock_listen, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (errno)
{
perror("绑定socket与地址失败");
return 0;
}


while(1)
{
/* 监听socket */

/*************************************************
函数原型:int listen(int sockfd, int backlog);
参数:
sockfd:要监听的socket的描述符
backlog:该socket上完成队列的最大长度。完成队列是指已完成三次握手(established),但尚未被服务器接受(accept)的客户机
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
listen(server_sock_listen, 0);
printf("Server is listening....\n");
if (errno)
{
perror("监听socket失败\n");
return 0;
}

/* 接收并显示消息 */

/*************************************************
函数原型:int accept(int sockfd, struct sockaddr *addr, int *addrlen);
参数:
sockfd:用于接受连接的socket的描述符
addr:客户机地址。此处为指向通用地址结构的指针,使用时,要进行强制类型转换。如果不关心客户机地址,可以设为NULL
addrlen:客户机地址结构大小。如果不关心,可以和addr一起设为NULL
返回值:
成功:返回一个新建的socket,用于数据传输
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
server_sock_data = accept(server_sock_listen, NULL, NULL);//服务器接受连接
printf("Accept......\n");
if (errno)
{
perror("服务器接受连接失败\n");
return 0;
}

while(1)
{
is_esc = 0;
memset(recv_msg, 0, sizeof(recv_msg)); //接收数组置零

/*************************************************
函数原型:int recv(int sockfd, void *buf, int len, int flags);
参数:
sockfd:用于接收数据的socket的描述符
buf:指向数据的指针
len:接收数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回接收的数据大小
失败:如果对方已关闭连接,返回0;其他错误返回-1,并在全局变量errno中记录错误类型
*************************************************/

/* 若连接断开则退出此客户机的连接,监听下一个客户机 */
if (recv(server_sock_data, recv_msg, sizeof(recv_msg), 0) == 0)
{
printf("Connection interrupted\n\n");
break;
}

printf("Recv: %s\n", recv_msg);
if (errno)
{
perror("显示消息失败");
return 0;
}

for(i = 0; i < strlen(recv_msg); i++)
{
if(recv_msg[i] == 27)
{
is_esc = 1;
break;
}
}
/* 接收到bye或者esc就断开连接 */
if (strcmp(recv_msg,stop_word) == 0 || is_esc )
{
printf("\n");
break;
}

/* 字符串逆序 */
strrev(recv_msg);

/* 发送消息 */
printf("Send: %s\n", recv_msg);

/*************************************************
函数原型:int send(int sockfd, const void *buf, int len, int flags);
接收4个参数:
sockfd:用于发送数据的socket的描述符
buf:指向数据的指针
len:发送数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回发送的数据大小
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
send(server_sock_data, recv_msg, strlen(recv_msg), 0);
if (errno)
{
perror("发送消息失败");
return 0;
}
}

/* 关闭数据socket */

/*************************************************
函数原型:int close(int sockfd);
参数:
sockfd:要关闭的socket的描述符
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
close(server_sock_data);
if (errno)
{
perror("关闭数据socket失败");
return 0;
}
}

/* 关闭监听socket */

close(server_sock_listen);
if (errno)
{
perror("关闭监听socket失败");
return 0;
}

return 0;
}

(3) 运行截图

image-20200322211841379

(4) 错误纠正

1°使用gets()函数输入消息报错

image-20200322211849237

原因在于gets()函数不会检查字符串的的长度,字符串过长会导致溢出,溢出的字符可能会覆盖一些重要的数据造成不可预料的后果,缓冲区溢出可能会作为蠕虫病毒的传播途径。

用fgets()替代

1
2
3
4
5
6
7
8
9
10
/* 不接收换行符的fgets()函数 */
void my_fgets(char data[], int count)
{
fgets(data, count, stdin);
/* 找出data中的"\n" */
char *find = strchr(data, '\n');
/* 截取换行符前的字符串 */
if(find)
*find = '\0';
}

2°含中文字符串逆序出现乱码

image-20200322211858348

image-20200322211904627

这是因为中文字符并不是一个字节,按照全英文字符翻转是错误的,遂将字符串翻转后对中文字符两位两位进行翻转

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
/* 字符串逆序函数 */
char *strrev(char *str)
{
char *p1, *p2;

if (! str || ! *str)
return str;
for (p1 = str, p2 = str + strlen(str) - 1; p2 > p1; ++p1, --p2)
{
*p1 ^= *p2;
*p2 ^= *p1;
*p1 ^= *p2;
}
/* UTF-8汉字一个是三个字节,进行相应逆序转化 */
for (p2 = str + strlen(str) - 1; p2 >= str; --p2)
{
if (*p2&0x80)//最高位为1
{

*p2 ^= *(p2-1);
*(p2-1) ^= *p2;
*p2 ^= *(p2-1);
p2--;
}
}
return str;
}

image-20200322211923677

image-20200322211928735

可以见到翻转结果依然错误,但是可以发现你好对应的是6个字符,可知一个汉字应该对应的是3个字符,后查阅资料得知UTF-8编码中一个汉字占3个字符,遂做下列改动

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
/* 字符串逆序函数 */
char *strrev(char *str)
{
char *p1, *p2;

if (! str || ! *str)
return str;
for (p1 = str, p2 = str + strlen(str) - 1; p2 > p1; ++p1, --p2)
{
*p1 ^= *p2;
*p2 ^= *p1;
*p1 ^= *p2;
}
/* UTF-8汉字一个是三个字节,进行相应逆序转化 */
for (p2 = str + strlen(str) - 1; p2 >= str; --p2)
{
if (*p2&0x80)//最高位为1
{

*p2 ^= *(p2-2);
*(p2-2) ^= *p2;
*p2 ^= *(p2-2);
p2 = p2-2;
}
}
return str;
}

成功解决问题!

3°客户机的突然连接中断会导致服务器陷入死循环

image-20200322211940332

原因是在客户机失联后服务器一直接收到空消息而陷入死循环,于是对第二个while(1)内做如下改动,利用recv()的返回值增加一个判断连接是否存在的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
while(1)
{
is_esc = 0;
memset(recv_msg, 0, sizeof(recv_msg)); //接收数组置零

/*************************************************
函数原型:int recv(int sockfd, void *buf, int len, int flags);
参数:
sockfd:用于接收数据的socket的描述符
buf:指向数据的指针
len:接收数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回接收的数据大小
失败:如果对方已关闭连接,返回0;其他错误返回-1,并在全局变量errno中记录错误类型
*************************************************/

/* 若连接断开则退出此客户机的连接,监听下一个客户机 */
if (recv(server_sock_data, recv_msg, sizeof(recv_msg), 0) == 0)
{
printf("Connection interrupted\n\n");
break;
}

同样对客户机增加判断服务器是否有连接的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 接收数组置零 */
memset(recv_msg, 0, sizeof(recv_msg));

/*************************************************
函数原型: int recv(int sockfd, void *buf, int len, int flags);
参数:
sockfd:用于接收数据的socket的描述符
buf:指向数据的指针
len:接收数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回接收的数据大小
失败:如果对方已关闭连接,返回0;其他错误返回-1,并在全局变量errno中记录错误类型
*************************************************/
if (recv(client_sock, recv_msg, sizeof(recv_msg), 0) == 0)
{
printf("Connection interrupted\n");
break;
}
printf("Server: %s\n", recv_msg);

image-20200322211950103

image-20200322211954902

问题解决!

Solution ②:

backlog:该socket上完成队列的最大长度。完成队列是指已完成三次握手(established),但尚未被服务器接受(accept)的客户机

使用端口为12345,使用netstat命令观察服务器socket状态:

1
netstat -an|grep 12345

其中显示的各列的含义如下

image-20200322212008955

Recv-Q表示的当前等待服务端调用接受完成三次握手的listen backlog数值

TCP有限状态机图

image-20200322212016601

当设置 backlog = 0 时

image-20200322212025109

当设置 backlog = 1 时

image-20200322212031306

当设置 backlog = 2 时

image-20200322212036654

当设置 backlog = 3 时

image-20200322212041952

由此可发现,对于服务器而言,state为SYN_RECV的状态个数为【客户机个数-backlog-2】(当客户机<backlog+2则为0),另外Recv-Q=backlog+1,结合Recv-Q的含义可知,实际在完成握手后未被服务器接受的客户机个数应该是backlog+1

因此若限定服务器只能accept一个客户机不能设置backlog=0,而是应该将监听socket关闭

对连接的客户机做一定的操作,设置 backlog = 1
做如下操作
服务器开启前
服务器开始运行
客户机1、2、3、4依次连接服务器
客户机1、2、3、4依次向服务器发送hi
客户机1、2、3、4依次发送bye断开与服务器的连接

(1)服务器开启前和服务开始运行

image-20200322212052833

LISTEN网络中所有主机

(2)客户机1、2、3、4依次连接服务器

image-20200322212106528

image-20200322212111683

对于客户机而言,客户机主动打开,发送SYN进入SYN_SENT,收到服务器发来的SYN、ACK后便认为其与服务器已经建立了连接,向服务器发送ACK,进入ESTABLISHED状态。
对于服务器而言,服务器被动打开,接收客户机发来的SYN后向客户机发送SYN、ACK,然后客户机向服务器发送ACK,首先客户机1被服务器ACCEPT,成功ESTABLISHED后进入数据传送阶段;客户机2、3也进入ESTABLISHED状态,但未被服务器ACCEPT,处于完成队列中,另外一个由于backlog限制,完成队列最大长度为2,客户机4的ACK未能被服务器接受而处于SYN_RECV状态。

(3)客户机1、2、3、4依次向服务器发送hi

image-20200322212121141

image-20200322212127621

观察到STATE并未改变

(4)客户机1、2、3、4依次发送bye断开与服务器的连接

image-20200322212135630

image-20200322212141237

对于服务器而言,由于客户机1断开连接,完成队列中的客户机2被ACCEPT,客户机4进入完成队列,显示状态为ESTABLISHED,而随着客户机的主动断开连接,服务器开始被动关闭连接,收到客户机发送的FIN后向客户机发送ACK进入CLOSE_WAIT状态,通过程序中的close(server_sock_data)命令向客户机发送FIN,进入LAST-ACK状态,再收到客户机的ACK便成功断开连接。
对于客户机而言,客户机主动关闭连接,向服务器发送FIN,进入FIN_WAIT_1状态,再收到服务器发来的ACK后进入FIN_WAIT_2状态,然后收到服务器发来的FIN后向服务器发送ACK进入TIME_WAIT状态,要定时经过两倍报文段寿命(2MSL)后才能完成断开与服务器的连接,相应的端口才会被回收。

Solution ③:

先运行下面命令

1
sudo sysctl net.ipv4.tcp_timestamps=0

在客户机代码中main()函数开头增加绑定客户机IP和端口语句,其中绑定端口作为mian()函数的参数传入

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
struct sockaddr_in client_addr;
/* 指定客户机地址 */

/* 用于socket创建通信连接的类型,这里就是ipv4地址类型的通信连接可用 */
client_addr.sin_family = AF_INET;
/* 整型变量从主机字节顺序转变成网络字节顺序:Big-Endian */
/* argv[3]从main函数接收,其是客户机要绑定的端口 */
client_addr.sin_port = htons(atoi(argv[3]));
/* 获取客户机机所有IP */
client_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/* 进行零填充 */
memset(client_addr.sin_zero, 0, sizeof(client_addr.sin_zero));
if (errno)
{
perror("指定客户机地址失败");
return 0;
}

/* 绑定socket与客户机地址 */
/*************************************************
函数原型: int bind(int sockfd, const struct sockaddr *addr, int addrlen);
参数:
sockfd:要绑定的socket的描述符
addr:要绑定的地址。此处为指向通用地址结构的指针,使用时,要进行强制类型转换
addrlen:地址结构大小。可使用sizeof自动计算
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
bind(client_sock, (struct sockaddr *)&client_addr, sizeof(client_addr));
if (errno)
{
perror("绑定socket与客户机地址失败");
return 0;
}

所选择客户机绑定的端口为22535

使用如下命令查看tcp端口状态

1
netstat -an|grep 22535

1.运行客户机连接服务器,由客户机主动close,再次运行客户机,观察结果

image-20200322212159305

可以发现当客户机close后,端口并不会马上被回收,而是处于TIME_WAIT的状态,再次用同一个客户机端口运行客户机程序则会在绑定阶段提示Address already in use的错误。

2、运行客户机连接服务器,由服务器主动close,再次运行客户机,观察结果

image-20200322212207616

可以发现服务器close后端口进入进入FIN_WAIT2状态,客户机端进入CLOSE_WAIT状态,继续运行客户机后跳出程序端口被释放,再次运行客户机程序并没有在绑定阶段提示端口占用的错误,仅在连接服务器部分报错

由此我们可以看出,如果在客户端的程序里,bind()了某个端口(比如22535),首先就要考虑这个端口是否被占用了,这大大增加实现的麻烦程度。其次如果端口号在程序中是固定值,那么该客户机就只能运行一个客户端,并且由上面我们也可以看出客户机不能使用同一个端口进行短时间的多次断线重连,这对使用者而言是不友好的。因此客户机不建议bind固定端口。

Solution ④:

由于客户端程序的inet_addr作用是将一个IP字符串转化为一个网络字节序的整数值,服务端程序的绑定IP地址是INADDR_ANY即0.0.0.0,网路字节序和主机字节序是一样的,因此下面对照试验不对IP地址进行改变。

我们在同一台linux主机下尝试以下几种情况:

(1)只有服务端的port不进行字节序转化

image-20200322212218841

(2)只有客户端的port不进行转化

image-20200322212225951

(3)客户端和服务端的port均不进行转化

image-20200322212236381

由此发现只有两端都进行网络字节序转化和都不进行网络字节序转化才可正常连通,这是因为作者使用的centos7.6的主机字节序是Little-Endian,而网络字节序是Big-Endian

IP地址和端口虽然没有作为数据传入send()、recv(),但是它们是间接传入这两个函数的:

image-20191126211038447image-20200322212306108

观察函数参数可以发现sockfd这个参数,其含义是某个socket的描述符,每个sockfd与socket进行一一对应,在服务端和客户端,每一个socket都会绑定相应的IP地址和端口号。由于不同主机的字节序可能不同,因此必须对发送的所有数据(包括IP和端口号)进行网络字节序转化。

主机字节序和CPU有关:Intel的x86系列采用Little-Endian,
其他如PowerPC 、SPARC和Motorola处理器则采用Big-Endian
网络字节序:TCP/IP各层协议将字节序定义为Big-Endian

如果不转化的话,通信双方就可能会无法建立连接,无法进行下一步的数据传输

任务2:字符串转换-网络服务(并发)

任务要求:

image-20200322212327702

Solution ①:

此任务只需修改服务器代码

(1) 服务器的源代码Server_fork.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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
/* 字符串逆序函数 */
char *strrev(char *str)
{
char *p1, *p2;

if (! str || ! *str)
return str;
for (p1 = str, p2 = str + strlen(str) - 1; p2 > p1; ++p1, --p2)
{
*p1 ^= *p2;
*p2 ^= *p1;
*p1 ^= *p2;
}
/* UTF-8汉字一个是三个字节,进行相应逆序转化 */
for (p2 = str + strlen(str) - 1; p2 >= str; --p2)
{
if (*p2&0x80)//最高位为1
{

*p2 ^= *(p2-2);
*(p2-2) ^= *p2;
*p2 ^= *(p2-2);
p2 = p2-2;
}
}
return str;
}
int main(int argc, char *argv[])
{
int server_sock_listen, server_sock_data;
struct sockaddr_in server_addr, client_addr;
int client_addrlen = sizeof(struct sockaddr_in);
char recv_msg[255];
char stop_word[]="bye";
int is_esc;
int i;
int fork_value;

/* 创建socket */

/*************************************************
函数原型: int socket(int family, int type, int protocol);
参数:
family:协议族。实验用AF_INET
type:socket类型。常见有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等
protocol:具体协议。0表示默认协议
返回值:
成功:返回一个新建的socket,用于数据传输
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
server_sock_listen = socket(AF_INET, SOCK_STREAM, 0);
if (errno)
{
perror("创建socket失败");
return 0;
}

/* 指定服务器地址 */
/* 用于socket创建通信连接的类型,这里就是ipv4地址类型的通信连接可用 */
server_addr.sin_family = AF_INET;
/* 整型变量从主机字节顺序转变成网络字节顺序:大头序 */
server_addr.sin_port = htons(atoi(argv[1]));
/* INADDR_ANY表示本机所有IP地址 */
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/* 进行零填充 */
memset(&server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));
if (errno)
{
perror("指定服务器地址失败");
return 0;
}

/* 绑定socket与地址 */

/*************************************************
函数原型: int bind(int sockfd, const struct sockaddr *addr, int addrlen);
参数:
sockfd:要绑定的socket的描述符
addr:要绑定的地址。此处为指向通用地址结构的指针,使用时,要进行强制类型转换
addrlen:地址结构大小。可使用sizeof自动计算
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
bind(server_sock_listen, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (errno)
{
perror("绑定socket与地址失败");
return 0;
}

/* 监听socket */

/*************************************************
函数原型:int listen(int sockfd, int backlog);
参数:
sockfd:要监听的socket的描述符
backlog:该socket上完成队列的最大长度。完成队列是指已完成三次握手(established),但尚未被服务器接受(accept)的客户机
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
listen(server_sock_listen, 0);
printf("Server is listening....\n");
if (errno)
{
perror("监听socket失败\n");
return 0;
}
while(1)
{
/* 接收并显示消息 */

/*************************************************
函数原型:int accept(int sockfd, struct sockaddr *addr, int *addrlen);
参数:
sockfd:用于接受连接的socket的描述符
addr:客户机地址。此处为指向通用地址结构的指针,使用时,要进行强制类型转换。如果不关心客户机地址,可以设为NULL
addrlen:客户机地址结构大小。如果不关心,可以和addr一起设为NULL
返回值:
成功:返回一个新建的socket,用于数据传输
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
server_sock_data = accept(server_sock_listen, (struct sockaddr *)&client_addr, &client_addrlen);//服务器接受连接
if (errno)
{
perror("服务器接受连接失败");
return 0;
}
printf("Accept %s:%d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);

/* 杀死僵尸进程 */
signal(SIGCHLD,SIG_IGN);
/* 创建子进程 */
fork_value = fork();
if (fork_value == -1)
{
perror("Fail to call to fork");
exit(1);
}
else if(fork_value == 0)
{
/* 关闭监听socket */
/*************************************************
函数原型:int close(int sockfd);
参数:
sockfd:要关闭的socket的描述符
返回值:
成功:返回0
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
/*首先关闭掉监听server_sock_listen,因为子进程并不需要监听,它只负责处理逻辑并发消息给客户端*/
close(server_sock_listen);
if (errno)
{
perror("关闭监听socket失败");
return 0;
}
while(1)
{
is_esc = 0;
memset(recv_msg, 0, sizeof(recv_msg)); //接收数组置零
/*************************************************
函数原型: int recv(int sockfd, void *buf, int len, int flags);
参数:
sockfd:用于接收数据的socket的描述符
buf:指向数据的指针
len:接收数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回接收的数据大小
失败:如果对方已关闭连接,返回0;其他错误返回-1,并在全局变量errno中记录错误类型
*************************************************/
if (recv(server_sock_data, recv_msg, sizeof(recv_msg), 0) == 0)
{
printf("The client %s:%d disconnected\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
break;
}
printf("From %s:%d: %s\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port, recv_msg);
if (errno)
{
perror("显示消息失败");
return 0;
}

for(i = 0; i < strlen(recv_msg); i++)
{
if(recv_msg[i] == 27)
{
is_esc = 1;
break;
}
}

if (strcmp(recv_msg,stop_word) == 0 || is_esc )
{
continue;//直接到下一循环的判断连接模块
}

strrev(recv_msg);//字符串逆序

/* 发送消息 */
printf("reply %s:%d: %s\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port, recv_msg);
/*************************************************
函数原型: int send(int sockfd, const void *buf, int len, int flags);
参数:
sockfd:用于发送数据的socket的描述符
buf:指向数据的指针
len:发送数据大小
flags:额外选项。本次实验设为0
返回值:
成功:返回发送的数据大小
失败:返回-1,并在全局变量errno中记录错误类型
*************************************************/
send(server_sock_data, recv_msg, strlen(recv_msg), 0);
if (errno)
{
perror("发送消息失败");
return 0;
}
}

/* 关闭数据socket */
close(server_sock_data);
if (errno)
{
perror("关闭数据socket失败");
return 0;
}
exit(0);
}
else
{
/* 关闭数据socket */
close(server_sock_data);
if (errno)
{
perror("关闭数据socket失败");
return 0;
}
}
}
/* 关闭监听socket */
close(server_sock_listen);
if (errno)
{
perror("关闭监听socket失败");
return 0;
}

return 0;
}

(2) 运行截图

1°在同一主机下

image-20200322212355639

2°在同一局域网的不同主机下

树莓派作为服务器端:

image-20200322212407992

手机的Termux和电脑虚拟机作为客户机分别运行

image-20200322212427094

NAT模式虚拟机作为客户机交互信息的原因:使用的是TCP协议,虚拟机向外发送信息,服务端接收,便会形成一条通路,主机对于虚拟机建立端口映射,而外部服务器便可以通过这条通路将信息送回NAT模式下的虚拟机客户机

(3) 错误纠正

任务2中服务器端需用到所传来消息的客户机对应的地址,需要用到accept()函数的后面第2个参数

image-20200322212434682

其中对于后两个参数,使用时必须要么全设为NULL,要么就都要传入非NULL的参数,只有用到第二个参数仍要对第三个参数传入正确的值,否则将会报错,正确使用如下

1
server_sock_data = accept(server_sock_listen, (struct sockaddr *)&client_addr, &client_addrlen);//服务器接受连接

Solution ②

父进程分支仍需要关闭该socket

使用端口为12345,使用netstat命令观察服务器socket状态:

1
netstat -an|grep 12345

(1)当父进程分支有关闭server_sock_data

首先服务端开启,三个客户端依次连接,然后依次发了“hi”消息

image-20200322212444303

接着客户机依次关闭,服务器再关闭

image-20200322212450300

可以发现当客户端断开连接,服务器继续运行的话可以端口可以正常释放,进入TIME_WAIT状态,经过两倍报文段寿命端口便会被释放

(2)当父进程分支没有关闭server_sock_data

首先服务端开启,三个客户端依次连接,然后依次发了“hi”消息

image-20200322212458251

接着客户机依次关闭,服务器再关闭

image-20200322212506437

可以发现当客户端断开连接,服务器继续运行,由TCP有限状态图,服务器进入被动关闭连接状态,而客户机处于主动关闭连接状态,此时,只有服务器执行close函数的操作,才能发送FIN,进入到LAST_ACK,但是服务器并没有执行close,对于服务器而言这些端口便会一直处于CLOST_WAIT状态,也就是socket无法被释放掉,而其所分配的描述符也不能被回收利用。

image-20200322212512970

如果有多个进程同时访问一个socket,close只关闭本进程对该socket的访问,不影响其他进程,当所有进程都close后,这个socket才被彻底清除,因此对于不仅要在子进程关闭socket,也要在父进程关闭socket

由于可分配的socket描述符是有限的,如果分配了以后不释放,也就是不能回收再利用,描述符最终会被耗尽。再而,原本服务端里父进程将和客户端连接的任务交给子进程之后就可以去accept下一个连接,但是如果父进程自己不关闭自己和客户端的连接,这个连接便会(被服务器认为)永远存在,直到服务端停止运行。

因此父进程分支仍需要关闭该socket

image-20200322212528943

image-20200322212534973

另外客户端为什么会从FIN_WAIT_2后就自动将端口释放了呢?从以下文章找到答案一个TCP FIN_WAIT2状态细节引发的感慨

image-20200322212543345

因此我们知道FIN_WAIT_2也是有超时机制的,其时间是180秒,并且连接在FIN_WAIT_2超时后并不会进入TIME_WAIT状态,也不会发送RESET,而是直接默默消失。

任务3:基于UDP socket的聊天室 (Python)

任务要求:

image-20200322212604984

基于udp socket的多人实时聊天室

Solution:

使用多线程的方法让客户端同时进行发送和接受消息

以下是包含两个继承了socket.socket的Server和Client类的两个模块

server.py

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
# -*- coding: utf-8 -*-
import socket
import sys,time,os

SPLIT = "|" #用于隔开用户名和消息以及判断是发送过来的信息是客户登录时的用户名还是聊天信息
BUFSIZE = 1024 #要接收的最大数据量

class Server(socket.socket):
'''这是一个继承socket.socket的类,用于实现服务端程序'''

def __init__(self,serv_address):
'''用父类socket.socket的初始化方法来初始化继承的属性'''
#socket.SOCK_DGRAM用于不可靠传输UDP
super(Server, self).__init__(socket.AF_INET,socket.SOCK_DGRAM)
self.__serv_address = serv_address#服务器地址
self.__clnt_address = dict()#客户机地址

def get_clnt_addr(self):
'''该函数用于访问私有变量self.__clnt_address'''
return self.__clnt_address

def send_msg(self, msg, address, Type):
'''该函数用于发送消息
msg是要发送的字符串
address是目的地址,形式为(ipaddr,port)的元组
Type是发送对象的类型,包括广播(代表系统发送的消息)、单播、多播
'''
if Type == 'Broadcast':
msg = 'Broadcast' + SPLIT + msg
print(self.__clnt_address,time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))#服务端打印发送时间
for name, address in self.__clnt_address.items():
self.sendto(msg.encode(encoding='UTF-8',errors='ignore'),address)

elif Type == 'Unicast':
#只发送给address
msg = 'Unicast' + SPLIT + msg
self.sendto(msg.encode(encoding='UTF-8',errors='ignore'), address)

elif Type=='Multicast':
#发送给所有人,但不包括服务端
for name, address in self.__clnt_address.items():
if address != self.__serv_address:
self.sendto(msg.encode(encoding='UTF-8',errors='ignore'), address)

def start(self):
'''该函数用于启动该服务端'''

#首先客户机对地址进行绑定
self.bind(self.__serv_address)
while True:
try:
data, addr = self.recvfrom(BUFSIZE)#接收信息
data = data.decode(encoding='UTF-8',errors='ignore')

if SPLIT not in data: #客户第一次登录,发送过来的只有不包含'|'的用户名

with open("user.txt", 'r+') as f:
#往user.txt添加客户名
user_list=f.read().split('\n')
while data in user_list:#如果用户名已存在就发送error代表客户名重复
self.sendto(b'error',addr)
data, addr = self.recvfrom(BUFSIZE)#重新接收客户名
data = data.decode(encoding='UTF-8',errors='ignore')
f.write(data+"\n")
self.__clnt_address[data] = addr
self.send_msg("欢迎%s进入聊天室...." % data,'','Broadcast')
self.send_msg("------------------%s------------------"%data,addr,'Unicast')#只发送给该用户

elif data.split(SPLIT)[1] == "exit":#传来消息为"exit"则代表有客户退出聊天室

self.__clnt_address.pop(data.split(SPLIT)[0])
self.send_msg("%s离开了聊天室...."%data.split(SPLIT)[0],'','Broadcast')#通知其他客户该客户离开聊天室
#在user.txt去除该客户名
with open("user.txt",'r') as f:
l=''.join(f.read().split(data.split(SPLIT)[0]+'\n'))
with open("user.txt",'w') as f:
f.write(l)

else:#其他消息就进行群发

self.send_msg(data, '','Multicast')

except ConnectionError as e:
print(e)
pass

self.close()

client.py

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
import socket
import sys
import threading

SPLIT = "|" #用于隔开用户名和消息以及判断是发送过来的信息是客户登录时的用户名还是聊天信息
BUFSIZE = 1024 #要接收的最大数据量

class Client(socket.socket):
'''这是一个继承socket.socket的类,用于实现客户端程序'''

def __init__(self, name, address):
'''用父类socket.socket的初始化方法来初始化继承的属性'''
#socket.SOCK_DGRAM用于不可靠传输UDP
super(Client, self).__init__(socket.AF_INET,socket.SOCK_DGRAM)
self.__name = name#客户名
self.__address = address#客户机地址

def start(self):
'''该函数用于启动该客户端'''

#首先进行服务器连接检测
try:
self.sendto(self.__name.encode(encoding='UTF-8',errors='ignore'), self.__address)
except :
print("----------服务器连接失败----------")

#客户名合法性检测
name_verify = self.recv(BUFSIZE).decode(encoding='UTF-8',errors='ignore')

while name_verify == "error":
name=input("该昵称已被用,请重新输入:")
while '\\' in name or '|' in name:
name = input("昵称存在非法字符,请重新输入:")
self.sendto(name.encode(encoding='UTF-8',errors='ignore'), self.__address)
self.__name = name
name_verify = self.recv(BUFSIZE).decode(encoding='UTF-8',errors='ignore')

t=threading.Thread(target=self.recv_msg)#创建另一个线程使得发送消息和接收消息不冲突
t.start()

while True:
word=input().strip()#发送消息
if not word:
continue # 避免回车导致服务器的死循环
self.send_msg(word)
if word == "exit":
self.close()
break

def send_msg(self,word):
'''该函数用于向客户端发送消息,消息包括用户名和实际内容,用'|'隔开'''

data = (self.__name + SPLIT + word).encode(encoding='UTF-8',errors='ignore')
self.sendto(data, self.__address)

def recv_msg(self):
'''该函数用于接收来自服务端的消息'''

while True:
try:
msg = self.recv(BUFSIZE)
self.show_msg(msg)

except ConnectionResetError as e:
print("----------服务器连接失败----------")

except OSError as f:
print("----------连接关闭----------")
break

def show_msg(self,data):
'''该函数用于向该客户展示信息内容,其中不显示客户自己发的信息'''
name, word = data.decode(encoding='UTF-8',errors='ignore').split(SPLIT)
if name == 'Broadcast'or name == 'Unicast':
print(word,'\n\n')
elif name!=self.__name:
print("%s : %s"%(name, word))

以下是服务端和客户端

new_server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from server import Server
import socket

def get_host_ip():
'''用于查询本机ip地址,返回值为ip'''

try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
finally:
s.close()

return ip

if __name__ == '__main__':
ip = get_host_ip()
print('服务器的ip:%s'%ip)
port = int(input('请输入要绑定的端口号:'))
s = Server(serv_address = (ip,port))
with open("user.txt",'r+') as f:
f.truncate()#清空user.txt
s.start()

new_client.py

1
2
3
4
5
6
7
8
9
from client import Client
import os
if __name__ == '__main__':
ip = input('请输入服务器的ip:')
port = int(input('请输入服务器绑定的端口号:'))
print('-----欢迎来到聊天室,退出聊天室请输入 “exit”-----' )
name = input("输入你的昵称: ")
c = Client(name = name, address = (ip,port))
c.start()

运行截图

在Windows环境下运行服务端和客户端代码

启动服务器以及本地一个客户端

image-20200322212631558

image-20200322212638517

另一台机子上进行客户端登录

image-20200322212645982

用户名重复

image-20200322212651589

用户退出

image-20200322212658430

捕捉的异常(服务器突然关闭)

image-20200322212704304

实验小结

本次实验课我通过了对socket编程的学习与运用,进一步加深了对传输层TCP和UDP传输协议的理解与认识。通过使用netstat命令,观察端口的使用状态,对于实验中产生的各种现象,可以通过TCP有限状态机图对其进行分析和理解。本次关于编程的实验我遇到不少疑难杂症,但是通过观察实验现象、上网查资料等方法均得到一一解决。此次实验收获良多,但在对代码的debug能力仍有不足,今后会加强这一方面的学习。