20 网络编程
OSI七层协议:
七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
五层划分为:应用层、传输层、网络层、数据链路层、物理层。
传输层:TCP协议和UDP协议
UDP(User Data Protocol)用户数据报协议, 是⼀个⽆连接的简单的⾯向数据报的传输层协议。 UDP不提供可靠性, 它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。 由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快。常见的有:语音通话、视频通话、实时游戏画面 等。
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,然后再进行收发数据。常见有:网站、手机APP数据获取等。
# 1. 简述 二层交换机 & 路由器 & 三层交换机 的作用。
"""
二层交换机:构建局域网并实现局域网内数据的转发。
路由器:实现跨局域网进行通信。
三层交换机:具备二层交换机和路由的功能。
2.简述常见词:IP、子网掩码、DHCP、公网IP、端口、域名的作用。
IP,本质上是一个32位的二进制,通过 . 等分为了4组8位二进制。
子网掩码,用于指定IP的网络地址和主机地址。
DHCP,网络设备中的一个服务,用于给接入当前网络的电脑自动设置 IP、子网掩码、网关。
公网IP,一般企业拉专线时会给固定的公网IP,只有具备公网IP才可以被互联网上的其他电脑访问。
端口,IP用于表示某台电脑,端口则表示此电脑上的具体程序。0-65535
域名,与IP构造对应关系,方便用户记忆。
软件架构:C/S : client server => 服务端
B/S : browser:浏览器 server => 服务端
本质:B/S架构也是C/S架构
C/S架构,是Client和Server的简称。开发这种架构的程序意味着你即需要开发客户端也需要开发服务端。
例如:你电脑的上QQ、百度网盘、钉钉、QQ音乐 等安装在电脑上的软件。服务端:互联网公司会开发一个程序放在他们的服务器上,用于给客户端提供数据支持。
客户端:大家在电脑安装的相关程序,内部会连接服务端进行收发数据并提供 交互和展示的功能。
B/S架构,是Browser和Server的简称。开发这种架构的程序意味着你开发服务端即可,客户端用用户电脑上的浏览器来代替。例如:淘宝、京东等网站。
服务端:互联网公司开发一个网站,放在他们的服务器上。
客户端:不需要开发,用现成的浏览器即可。
简而言之,B/S架构就是开发网站;C/S架构就是开发安装在电脑的软件。
TCP和UDP协议
TCP协议的特点:
1. 数据可靠传输
2. 速度相对于UDP协议较慢
UDP协议的特点:
1. 数据不可靠
2. 速度相对于TCP协议较快
TCP协议的三次握手和四次挥手
- 最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。
- TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
- TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
- TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
- TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
- 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
- 数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗ *∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
本机的IP地址:127.0.0.1
socket抽象层:就是一个个的对外访问的接口
socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
我们经常把Socket翻译为套接字,Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用
套接字分类
- 基于文件类型的套接字家族:AF_UNIX
- 基于网络类型的套接字家族:AF_INE
套接字工作流程
套接字函数
一、服务端套接字函数
- s.bind() 绑定(主机,端口号)到套接字
- s.listen() 开始TCP监听
- s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
二、客户端套接字函数
- s.connect() 主动初始化TCP服务器连接
- s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
三、公共用途的套接字函数
- s.recv() 接收TCP数据
- s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
- s.sendall() 发送完整的TCP数据(本质就是循环调用
- send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
- s.recvfrom() 接收UDP数据
- s.sendto() 发送UDP数据
- s.getpeername() 连接到当前套接字的远端的地址
- s.getsockname() 当前套接字的地址
- s.getsockopt() 返回指定套接字的参数
- s.setsockopt() 设置指定套接字的参数
- s.close() 关闭套接字
四、面向锁的套接字方法
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
五、面向文件的套接字的函数
- s.fileno() 套接字的文件描述符
- s.makefile() 创建一个与该套接字相关的文件
- 六、基于tcp的套接字
-
服务端 import socket # 1. 实例化socket对象 # 参数不传递默认代表是TCP协议 server = socket.socket(type=socket.SOCK_STREAM) # 2. 绑定 server.bind(('127.0.0.1', 8001)) # 3. 监听, 括号中是半连接池 server.listen(5) # 4. 接收客户端发来的信息 print("正在等待客户端发来的消息:") # sock:当次链接对象,addr:客户端的地址, ip+port sock, addr = server.accept() # 5. 拿到数据, 一次最多接收1024个字节的数据 data = sock.recv(1024) print(data) # 6. 发送客户端的数据 sock.send(data.upper()) # 7. 断开链接 sock.close() # 8. server.close() 客户端 import socket client = socket.socket() client.connect(('127.0.0.1',8001)) # 向服务端发送数据 client.send(b'hello baby') # 接收服务端发来的数据 data = client.recv(1024) print(data) client.close()
实现通信循环
-
服务端 import socket # 1. 实例化socket对象 # 参数不传递默认代表是TCP协议 server = socket.socket(type=socket.SOCK_STREAM) # 2. 绑定 server.bind(('127.0.0.1', 8002)) # 3. 监听, 括号中是半连接池 server.listen(5) # 4. 接收客户端发来的信息 print("正在等待客户端发来的消息:") while True: # sock:当次链接对象,addr:客户端的地址, ip+port sock, addr = server.accept() while True: # 5. 拿到数据, 一次最多接收1024个字节的数据 try: data = sock.recv(1024) print(data) # 6. 发送客户端的数据 sock.send(data.upper()) except Exception as e: print(e) break # 7. 断开链接 sock.close() # 8. server.close() 客服端 import socket client = socket.socket() client.connect(('127.0.0.1',8002)) while True: # 向服务端发送数据 input_data = input('请输入数据:').strip() # client.send(b'hello baby') client.send(input_data.encode('utf-8')) # 接收服务端发来的数据 data = client.recv(1024) print(data) client.close()
- 七、基于udp的套接字
-
服务端 import socket # SOCK_STREAM : tcp协议的服务端 # SOCK_DGRAM :udp协议的服务端 server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》UDP server.bind(('127.0.0.1', 8080)) while True: data, client_addr = server.recvfrom(1024) print('===>', data, client_addr) server.sendto(data.upper(), client_addr) server.close() 客户端 import socket client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》UDP while True: msg = input('>>: ').strip() # msg='' client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080)) data, server_addr = client.recvfrom(1024) print(data) client.close()
-
两台电脑在进行收发数据时,其实不是直接将数据传输给对方。
-
对于发送者,执行
sendall/send
发送消息时,是将数据先发送至自己网卡的 写缓冲区 ,再由缓冲区将数据发送给到对方网卡的读缓冲区。 -
对于接受者,执行
recv
接收消息时,是从自己网卡的读缓冲区获取数据。
所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。例如:
-
-
# socket客户端(发送者) import socket client = socket.socket() client.connect(('127.0.0.1', 8001)) client.sendall('alex正在吃'.encode('utf-8')) client.sendall('翔'.encode('utf-8')) client.close() # socket服务端(接收者) import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) conn, addr = sock.accept() client_data = conn.recv(1024) print(client_data.decode('utf-8')) conn.close() sock.close()
- 如何解决粘包的问题?
-
-
发送数据,先发送数据的长度,再发送数据(或拼接起来再发送)。
-
接收数据,先读4个字节就可以知道自己这个数据包中的数据长度,再根据长度读取到数据。
-
-
import struct # ########### 数值转换为固定4个字节,四个字节的范围 -2147483648 <= number <= 2147483647 ########### v1 = struct.pack('i', 199) print(v1) # b'\xc7\x00\x00\x00' for item in v1: print(item, bin(item)) # ########### 4个字节转换为数字 ########### v2 = struct.unpack('i', v1) # v1= b'\xc7\x00\x00\x00' print(v2) # (199,)