Socket套接字编程
一:socket的通信流程介绍
1.什么是Socket
socket是应用层 与 传输层 中间的软件抽象层,是一组接口。
在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面.
对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
2.Socket套接字的发展史
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。
因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。
一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯,这也被称进程间通讯或 IPC。
套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
①基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
②基于网络类型的套接字家族
套接字家族的名字:AF_INET
还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET。
3.TCP与UDP
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)
①话不多说,上图:
②UDP详解
UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
1.面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
-
在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
-
在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
2. 有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
3. UDP是面向报文的
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
4. 不可靠性
首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。
即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
5.头部开销小,传输数据报文时是很高效的
UDP 头部包含了以下几个数据:
- 2个十六位的端口号,分别为源端口(可选字段)和目标端口
- 整个数据报文的长度
- 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
因此 UDP 的头部开销小,只有8字节,相比 TCP 的至少20字节要少得多,在传输数据报文时是很高效的
③TCP详解
当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。
例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。
TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。
TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。
1.三次握手 与 四次挥手
详细请移步:https://www.cnblogs.com/xuexianqi/p/13493126.html
2.面向连接
面向连接,是指发送数据之前必须在两端建立连接。
建立连接的方法是“三次握手”,这样能建立可靠的连接。
建立连接,是为数据的可靠传输打下了基础。
3.仅支持单播传输
每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。
4.面向字节流
TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
5.可靠传输
对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。
TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
6.提供拥塞控制
当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞
7.TCP提供全双工通信
TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。
当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)
④TCP与UDP对比
对比 | UDP | TCP |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
⑤查看网络接口信息:
netstat -an
netstat -an |findstr 8080
二:基于TCP协议通信的套接字程序
服务端要满足的特性:
1.一直对外提供服务
2.并发地提供服务
1.基础版
server.py
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.插手机卡
server_ip = '127.0.0.1'
server_port = 8080
phone.bind((server_ip, server_port)) # 127.0.0.1:本地回环地址,只能本机访问
# 3.开机
phone.listen(5) # backlog:半连接池
print(f'服务端开始启动,IP:{server_ip},端口:{server_port}')
# 4.等电话连接 - 循环
while True:
conn, client_addr = phone.accept() # conn代表双向连接,可以收数据,也可以发数据;client_addr:客户端地址
print(f'客户端地址:{client_addr}')
# 5.收消息
while True:
try:
recv_data = conn.recv(1024) # 1024:最大接收的字节个数,超过1024个的字节,下次发送
data = recv_data.decode('UTF-8')
print('收到的客户端的数据:', data)
conn.send(recv_data.upper()) # 此处的data是bytes类型,可以直接转换成大写
print(f'服务端将接收的数据:{data} 转成了大写,发给了客户端')
except Exception:
print('出现了异常')
break
# 6.关闭
conn.close() # 挂掉电话
phone.close() # 关机
client.py
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.拨通电话
phone.connect(('127.0.0.1', 8080))
# 3.发消息 - 循环
while True:
msg = input('请输入要发送的消息:').strip()
phone.send(msg.encode('UTF-8'))
print(f'客户端发送了数据:{msg}')
# 4.收消息
data = phone.recv(1024)
print('服务端返回的数据:', data.decode('UTF-8'))
# 5.关闭连接
phone.close()
2.优化版
server.py
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.插手机卡
server_ip = '127.0.0.1'
server_port = 8080
phone.bind((server_ip, server_port)) # 127.0.0.1:本地回环地址,只能本机访问
# 3.开机
phone.listen(5) # backlog:半连接池
print(f'服务端开始启动,IP:{server_ip},端口:{server_port}')
# 4.等电话连接
conn, client_addr = phone.accept() # conn代表双向连接,可以收数据,也可以发数据;client_addr:客户端地址
print('服务端启动成功,等待客户端的接入')
# 5.收消息
recv_data = conn.recv(1024) # 1024:最大接收的字节个数,超过1024个的字节,下次发送
data = recv_data.decode('UTF-8')
print('收到的客户端的数据:', data)
# 6.发消息
conn.send(recv_data.upper()) # 此处的data是bytes类型,可以直接转换成大写
print(f'服务端将接收的数据:{data} 转成了大写,发给了客户端')
# 7.关闭
conn.close() # 挂掉电话
phone.close() # 关机
# 输出:
# 服务端开始启动,IP:127.0.0.1,端口:8080
# 服务端启动成功,等待客户端的接入
# 收到的客户端的数据: Hello World
# 服务端将接收的数据:Hello World 转成了大写,发给了客户端
client.py
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.拨通电话
phone.connect(('127.0.0.1', 8080))
# 3.发消息
msg = 'Hello World'
phone.send(msg.encode('UTF-8'))
print(f'客户端发送了数据:{msg}')
# 4.收消息
data = phone.recv(1024)
print('服务端返回的数据:', data.decode('UTF-8'))
# 5.关闭连接
phone.close()
# 输出:
# 客户端发送了数据:Hello World
# 服务端返回的数据: HELLO WORLD
3.添加循环、检测异常
server.py
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.插手机卡
server_ip = '127.0.0.1'
server_port = 8080
# phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用端口资源,但是不建议用
phone.bind((server_ip, server_port)) # 127.0.0.1:本地回环地址,只能本机访问
# 3.开机
phone.listen(5) # backlog:半连接池
print(f'服务端开始启动,IP:{server_ip},端口:{server_port}')
# 4.等电话连接
while True:
conn, client_addr = phone.accept() # conn代表双向连接,可以收数据,也可以发数据;client_addr:客户端地址
print(f'客户端地址:{client_addr}')
# 5.收消息
while True:
try:
recv_data = conn.recv(1024) # 1024:最大接收的字节个数,超过1024个的字节,下次发送
data = recv_data.decode('UTF-8')
if len(data) == 0: # 针对Linux系统
break
print('收到的客户端的数据:', data)
conn.send(recv_data.upper()) # 此处的data是bytes类型,可以直接转换成大写
print(f'服务端将接收的数据[{data}] 转成了大写,发给了客户端')
except Exception: # 针对Windows系统
print(f'客户端{client_addr}异常断开')
break
# 6.关闭
conn.close() # 挂掉电话
phone.close() # 关机
client.py
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.拨通电话
phone.connect(('127.0.0.1', 8080))
# 3.发消息
while True:
msg = input('请输入要发送的消息:').strip()
phone.send(msg.encode('UTF-8'))
print(f'客户端发送了数据:{msg},等待服务器的响应...')
# 4.收消息
data = phone.recv(1024)
print('服务端返回的数据:', data.decode('UTF-8'))
# 5.关闭连接
phone.close()
4.远程控制
服务端开启后,客户端可以直接输入命令控制服务端,服务端返回输出的值
remote_server.py
import socket
import subprocess
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.插手机卡
server_ip = '127.0.0.1'
server_port = 8080
# phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用端口资源,但是不建议用
phone.bind((server_ip, server_port)) # 127.0.0.1:本地回环地址,只能本机访问
# 3.开机
phone.listen(5) # backlog:半连接池
print(f'服务端开始启动,IP:{server_ip},端口:{server_port}')
# 4.等电话连接
while True:
conn, client_addr = phone.accept() # conn代表双向连接,可以收数据,也可以发数据;client_addr:客户端地址
print(f'客户端地址:{client_addr}')
# 5.收消息
while True:
try:
recv_cmd = conn.recv(1024) # 1024:最大接收的字节个数,超过1024个的字节,下次发送
if len(recv_cmd) == 0: # 针对Linux系统
break
cmd = recv_cmd.decode('UTF-8')
print('收到的客户端的数据:', cmd)
obj = subprocess.Popen(cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
res = obj.stdout.read() + obj.stderr.read() # 能不能不拼接?
conn.send(res)
except Exception: # 针对Windows系统
print(f'客户端{client_addr}异常断开')
break
# 6.关闭
conn.close() # 挂掉电话
phone.close() # 关机
remote_client.py
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM --> TCP协议(流式协议)
# 2.拨通电话
phone.connect(('127.0.0.1', 8080))
# 3.发消息
while True:
cmd = input('请输入要发送的消息:').strip()
phone.send(cmd.encode('UTF-8'))
print(f'客户端输入命令:{cmd},等待服务器的响应...')
# 4.收消息
recv_data = phone.recv(1024)
data = recv_data.decode('GBK')
print('服务端返回的数据:', data)
# 5.关闭连接
phone.close()