TCP 和 UDP 协议
一、socket层
-
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
-
简而言之:
其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。
所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
-
套接字得发展史: (套接字分为两类,文件类型和网络类型)
-
(文件)套接字家族的名字:AF_UNIX(起源于UNIX)
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
-
(网络)套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
-
-
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)
-
具体的三次握手,四次挥手过程:
-
三次握手:
- 第一次握手:主机A发送位码为syn=1,随机产生seq number=x的数据包(以便验证服务端是否收到,收到会返回加一数)到服务器(B),客户端进入
SYN_SEND
状态,等待服务器(B)的确认;主机B由SYN=1知道,A要求建立联机; - 第二次握手:主机B收到请求后要确认联机信息,向A发送ack number(主机A的seq+1),syn=1,ack=1,随机产生seq=y的包,此时服务器(B)进入
SYN_RECV
状态; - 第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。客户端和服务器端都进入
ESTABLISHED
状态,完成TCP三次握手
- 第一次握手:主机A发送位码为syn=1,随机产生seq number=x的数据包(以便验证服务端是否收到,收到会返回加一数)到服务器(B),客户端进入
-
问题一:为什么连接建立需要三次握手,而不是两次握手?
- 答:防止失效的连接请求报文段被服务端接收,从而产生错误,也就是说要确认客户端还活着。若建立连接只需两次握手,客户端并没有太大的变化,仍然需要获得服务端的应答后才进入ESTABLISHED状态,而服务端在收到连接请求后就进入ESTABLISHED状态。此时如果网络拥塞,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收并确认应答,双方便开始通信,通信结束后释放连接。此时,如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入ESTABLISHED状态,等待发送数据或主动发送数据。但此时的客户端早已进入CLOSED状态,服务端将会一直等待下去,这样浪费服务端连接资源。
-
四次挥手:
- 第一次挥手:主机1(可以使客户端,也可以是服务器端),设置
Sequence Number
和Acknowledgment Number
,向主机2发送一个FIN
报文段;此时,主机1进入FIN_WAIT_1
状态;这表示主机1没有数据要发送给主机2了; - 第二次挥手:主机2收到了主机1发送的
FIN
报文段,向主机1回一个ACK
报文段,Acknowledgment Number
为Sequence Number
加1;主机1进入FIN_WAIT_2
状态;主机2告诉主机1,我也没有数据要发送了,可以进行关闭连接了; - 第三次挥手:主机2向主机1发送
FIN
报文段,请求关闭连接,同时主机2进入CLOSE_WAIT
状态; - 第四次挥手:主机1收到主机2发送的
FIN
报文段,向主机2发送ACK
报文段,然后主机1进入TIME_WAIT
状态;主机2收到主机1的ACK
报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了
- 第一次挥手:主机1(可以使客户端,也可以是服务器端),设置
-
问题一:.为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
- 答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文
-
问题二:lient发送完最后一个ack之后,进入time_wait状态,但是他怎么知道server有没有收到这个ack呢?莫非sever也要等待一段时间,如果收到了这个ack就close,如果没有收到就再发一个fin给client?这么说server最后也有一个time_wait哦?求解答
-
答:因为网络原因,主动关闭的一方发送的这个ACK包很可能延迟,从而触发被动连接一方重传FIN包。极端情况下,这一去一回,就是两倍的MSL时长。如果主动关闭的一方跳过TIME_WAIT直接进入CLOSED,或者在TIME_WAIT停留的时长不足两倍的MSL,那么当被动
关闭的一方早先发出的延迟包到达后,就可能出现类似下面的问题:1.旧的TCP连接已经不存在了,系统此时只能返回RST包2.新的TCP连接被建立起来了,延迟包可能干扰新的连接,这就是为什么time_wait需要等待2MSL时长的原因。
-
-
问题三:为什么要四次挥手?
-
答:确保数据能够完整传输。
当被动方收到主动方的FIN报文通知时,它仅仅表示主动方没有数据再发送给被动方了。但未必被动方所有的数据都完整的发送给了主动方,所以被动方不会马上关闭SOCKET,它可能还需要发送一些数据给主动方后,再发送FIN报文给主动方,告诉主动方同意关闭连接,所以这里的ACK报文和FIN报文多数情况下都是分开发送的
-
-
-
xxxxxxxxxx
总结:
TCP协议:
1、 面向连接\可靠\慢\对传递的数据的长短没有要求
2、 两台机器之间要想传递信息必须先建立连接
3、 之后在有了连接的基础上,进行信息的传递
4、可靠 : 数据不会丢失 不会重复被接收
5、慢 : 每一次发送的数据还要等待结果
6、 三次握手和四次挥手
UDP协议:
1、无连接\不可靠\快\不能传输过长的数据0
2、机器之间传递信息不需要建立连接 直接发就行
3、不可靠 : 数据有可能丢失
五层协议:
1、应用层 python send(b'hello,world')
# socket(虚拟,对下面的进行了整合,直接使用)
2、传输层 端口 tcp/udp协议 四层路由器 四层交换机
3、网络层 ip地址相关 ip协议 路由器 三层交换机
4、数据链路层 mac地址相关 arp协议 网卡 二层交换机
5、物理层 网线
二、套接字的使用
-
基于TCP协议的socket使用:
x#基础服务端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息,需要时要解码decode
conn.send(b'hi') #向客户端发送信息,编码必要时
conn.close() #关闭客户端套接字,不是closed
sk.close() #关闭服务器套接字(可选)
#基础客服端
import socket
sk = socket.socket() # 创建客户套接字
sk.connect(('127.0.0.1',8898)) # 尝试连接服务器,0,0,0,1是全网段,所有来防本机的都可以
sk.send(b'hello!')
ret = sk.recv(1024) # 对话(发送/接收)
print(ret)
sk.close() # 关闭客户套接字
#注意,在服务端启动时,如果遇到,Address alreadly in use,则需要在bind前加sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
#TCP的聊天版本服务端:可多连接,但是必须一对一回复
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.listen()
while True:
conn,addr = sk.accept() # 等待用户来连接我
while True:
msg = input('>>>')
conn.send(msg.encode('utf-8'))
if msg.upper() == 'Q':
break
content = conn.recv(1024).decode('utf-8') # 等待 客户端给我φ消息
if content.upper() == 'Q': break
print(content)
conn.close()
sk.close()
#TCP客户端:
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9001))
while True:
ret = sk.recv(1024).decode('utf-8')
if ret.upper() == 'Q':break
print(ret)
msg = input('>>>')
sk.send(msg.encode('utf-8'))
if msg.upper() == 'Q':
break
sk.close()
-
基于UDP协议的socket的使用:
xxxxxxxxxx
#基础服务端
import socket
udp_sk= socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024) # 如果有from则需要addr接收地址
print(msg)
udp_sk.sendto(b'hi',addr) # 对话(接收与发送)
udp_sk.close()
#基础客服端
import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)
#进阶服务端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',9001))
while True:
msg,client_addr = sk.recvfrom(1024)
print(msg.decode('utf-8'))
content = input('>>>')
sk.sendto(content.encode('utf-8'),client_addr)
sk.close()
#进阶客户端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1',9001)
while True:
content = input('>>>')
if content.upper() == 'Q':break
sk.sendto(content.encode('utf-8'),server_addr)
msg = sk.recv(1024).decode('utf-8')
if msg.upper() == 'Q':break
print(msg)
sk.close()
#高级QQ服务端
import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_server_sock.bind(ip_port)
while True:
qq_msg,addr=udp_server_sock.recvfrom(1024)
print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
back_msg=input('回复消息: ').strip()
udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
#高级QQ服务端
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
qq_name_dic={
'金老板':('127.0.0.1',8081),
'哪吒':('127.0.0.1',8081),
'egg':('127.0.0.1',8081),
'yuan':('127.0.0.1',8081),
}
while True:
qq_name=input('请选择聊天对象: ').strip()
while True:
msg=input('请输入消息,回车发送,输入q结束和他的聊天: ').strip()
if msg == 'q':break
if not msg or not qq_name or qq_name not in qq_name_dic:continue
udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
udp_client_socket.close()
三、socket的相关方法:
xxxxxxxxxx
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据
s.sendall() 发送TCP数据,发送一个字符串套接字,知道所有都发完,递归版的send
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 返回套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件