实验目的
实验内容与分析
实验环境
CentOS 7.7 + Python3.6.8
任务1:实现字符串逆序回送
任务要求:
Solution ①:
(1) 客户机的源代码client.c
1 |
|
(2) 服务器的源代码server.c
1 |
|
(3) 运行截图
(4) 错误纠正
1°使用gets()函数输入消息报错
原因在于gets()函数不会检查字符串的的长度,字符串过长会导致溢出,溢出的字符可能会覆盖一些重要的数据造成不可预料的后果,缓冲区溢出可能会作为蠕虫病毒的传播途径。
用fgets()替代
1 | /* 不接收换行符的fgets()函数 */ |
2°含中文字符串逆序出现乱码
这是因为中文字符并不是一个字节,按照全英文字符翻转是错误的,遂将字符串翻转后对中文字符两位两位进行翻转
1 | /* 字符串逆序函数 */ |
可以见到翻转结果依然错误,但是可以发现你好对应的是6个字符,可知一个汉字应该对应的是3个字符,后查阅资料得知UTF-8编码中一个汉字占3个字符,遂做下列改动
1 | /* 字符串逆序函数 */ |
成功解决问题!
3°客户机的突然连接中断会导致服务器陷入死循环
原因是在客户机失联后服务器一直接收到空消息而陷入死循环,于是对第二个while(1)内做如下改动,利用recv()的返回值增加一个判断连接是否存在的语句
1 | while(1) |
同样对客户机增加判断服务器是否有连接的语句
1 | /* 接收数组置零 */ |
问题解决!
Solution ②:
backlog:该socket上完成队列的最大长度。完成队列是指已完成三次握手(established),但尚未被服务器接受(accept)的客户机
使用端口为12345,使用netstat命令观察服务器socket状态:
1 | netstat -an|grep 12345 |
其中显示的各列的含义如下
Recv-Q表示的当前等待服务端调用接受完成三次握手的listen backlog数值
TCP有限状态机图
当设置 backlog = 0 时
当设置 backlog = 1 时
当设置 backlog = 2 时
当设置 backlog = 3 时
由此可发现,对于服务器而言,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)服务器开启前和服务开始运行
LISTEN网络中所有主机
(2)客户机1、2、3、4依次连接服务器
对于客户机而言,客户机主动打开,发送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
观察到STATE并未改变
(4)客户机1、2、3、4依次发送bye断开与服务器的连接
对于服务器而言,由于客户机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 | struct sockaddr_in client_addr; |
所选择客户机绑定的端口为22535
使用如下命令查看tcp端口状态
1 | netstat -an|grep 22535 |
1.运行客户机连接服务器,由客户机主动close,再次运行客户机,观察结果
可以发现当客户机close后,端口并不会马上被回收,而是处于TIME_WAIT的状态,再次用同一个客户机端口运行客户机程序则会在绑定阶段提示Address already in use的错误。
2、运行客户机连接服务器,由服务器主动close,再次运行客户机,观察结果
可以发现服务器close后端口进入进入FIN_WAIT2状态,客户机端进入CLOSE_WAIT状态,继续运行客户机后跳出程序端口被释放,再次运行客户机程序并没有在绑定阶段提示端口占用的错误,仅在连接服务器部分报错
由此我们可以看出,如果在客户端的程序里,bind()了某个端口(比如22535),首先就要考虑这个端口是否被占用了,这大大增加实现的麻烦程度。其次如果端口号在程序中是固定值,那么该客户机就只能运行一个客户端,并且由上面我们也可以看出客户机不能使用同一个端口进行短时间的多次断线重连,这对使用者而言是不友好的。因此客户机不建议bind固定端口。
Solution ④:
由于客户端程序的inet_addr作用是将一个IP字符串转化为一个网络字节序的整数值,服务端程序的绑定IP地址是INADDR_ANY即0.0.0.0,网路字节序和主机字节序是一样的,因此下面对照试验不对IP地址进行改变。
我们在同一台linux主机下尝试以下几种情况:
(1)只有服务端的port不进行字节序转化
(2)只有客户端的port不进行转化
(3)客户端和服务端的port均不进行转化
由此发现只有两端都进行网络字节序转化和都不进行网络字节序转化才可正常连通,这是因为作者使用的centos7.6的主机字节序是Little-Endian,而网络字节序是Big-Endian。
IP地址和端口虽然没有作为数据传入send()、recv(),但是它们是间接传入这两个函数的:
观察函数参数可以发现sockfd这个参数,其含义是某个socket的描述符,每个sockfd与socket进行一一对应,在服务端和客户端,每一个socket都会绑定相应的IP地址和端口号。由于不同主机的字节序可能不同,因此必须对发送的所有数据(包括IP和端口号)进行网络字节序转化。
主机字节序和CPU有关:Intel的x86系列采用Little-Endian,
其他如PowerPC 、SPARC和Motorola处理器则采用Big-Endian
网络字节序:TCP/IP各层协议将字节序定义为Big-Endian
如果不转化的话,通信双方就可能会无法建立连接,无法进行下一步的数据传输
任务2:字符串转换-网络服务(并发)
任务要求:
Solution ①:
此任务只需修改服务器代码
(1) 服务器的源代码Server_fork.c
1 |
|
(2) 运行截图
1°在同一主机下
2°在同一局域网的不同主机下
树莓派作为服务器端:
手机的Termux和电脑虚拟机作为客户机分别运行
NAT模式虚拟机作为客户机交互信息的原因:使用的是TCP协议,虚拟机向外发送信息,服务端接收,便会形成一条通路,主机对于虚拟机建立端口映射,而外部服务器便可以通过这条通路将信息送回NAT模式下的虚拟机客户机
(3) 错误纠正
任务2中服务器端需用到所传来消息的客户机对应的地址,需要用到accept()函数的后面第2个参数
其中对于后两个参数,使用时必须要么全设为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”消息
接着客户机依次关闭,服务器再关闭
可以发现当客户端断开连接,服务器继续运行的话可以端口可以正常释放,进入TIME_WAIT状态,经过两倍报文段寿命端口便会被释放
(2)当父进程分支没有关闭server_sock_data
首先服务端开启,三个客户端依次连接,然后依次发了“hi”消息
接着客户机依次关闭,服务器再关闭
可以发现当客户端断开连接,服务器继续运行,由TCP有限状态图,服务器进入被动关闭连接状态,而客户机处于主动关闭连接状态,此时,只有服务器执行close函数的操作,才能发送FIN,进入到LAST_ACK,但是服务器并没有执行close,对于服务器而言这些端口便会一直处于CLOST_WAIT状态,也就是socket无法被释放掉,而其所分配的描述符也不能被回收利用。
如果有多个进程同时访问一个socket,close只关闭本进程对该socket的访问,不影响其他进程,当所有进程都close后,这个socket才被彻底清除,因此对于不仅要在子进程关闭socket,也要在父进程关闭socket
由于可分配的socket描述符是有限的,如果分配了以后不释放,也就是不能回收再利用,描述符最终会被耗尽。再而,原本服务端里父进程将和客户端连接的任务交给子进程之后就可以去accept下一个连接,但是如果父进程自己不关闭自己和客户端的连接,这个连接便会(被服务器认为)永远存在,直到服务端停止运行。
因此父进程分支仍需要关闭该socket
另外客户端为什么会从FIN_WAIT_2后就自动将端口释放了呢?从以下文章找到答案一个TCP FIN_WAIT2状态细节引发的感慨
因此我们知道FIN_WAIT_2也是有超时机制的,其时间是180秒,并且连接在FIN_WAIT_2超时后并不会进入TIME_WAIT状态,也不会发送RESET,而是直接默默消失。
任务3:基于UDP socket的聊天室 (Python)
任务要求:
基于udp socket的多人实时聊天室
Solution:
使用多线程的方法让客户端同时进行发送和接受消息
以下是包含两个继承了socket.socket的Server和Client类的两个模块
server.py
1 | # -*- coding: utf-8 -*- |
client.py
1 | import socket |
以下是服务端和客户端
new_server.py
1 | from server import Server |
new_client.py
1 | from client import Client |
运行截图
在Windows环境下运行服务端和客户端代码
启动服务器以及本地一个客户端
另一台机子上进行客户端登录
用户名重复
用户退出
捕捉的异常(服务器突然关闭)
实验小结
本次实验课我通过了对socket编程的学习与运用,进一步加深了对传输层TCP和UDP传输协议的理解与认识。通过使用netstat命令,观察端口的使用状态,对于实验中产生的各种现象,可以通过TCP有限状态机图对其进行分析和理解。本次关于编程的实验我遇到不少疑难杂症,但是通过观察实验现象、上网查资料等方法均得到一一解决。此次实验收获良多,但在对代码的debug能力仍有不足,今后会加强这一方面的学习。