前言
互联网时代,现在基本上所有的程序都是网络程序,很少有单机版的程序了。网络编程就是如何在程序中实现两台计算机的通信。
Python语言中,提供了大量的内置模块和第三方模块用于支持各种网络访问,而且Python语言在网络通信方面的优点特别突出,远远
领先其他语言。
通过本章,可以学到:
1 了解TCP和UDP
2 掌握编写UDP Socket客户端应用
3 掌握编写UDP Socket服务器端应用
4 掌握编写TCP Socket客户端应用
5 掌握编写TCP Socket服务器端应用
一、大白话OSI七层协议
互联网的本质就是一系列的网络协议,这个协议就叫OSI协议(一系列协议),按照功能不同,分工不同,人为的分层七层。实际上这个七层是不存在的。没有这七层的概念,只是人为的划分而已。区分出来的目的只是让你明白哪一层是干什么用的。
每一层都运行不同的协议。协议是干什么的,协议就是标准。
实际上还有人把它划成五层、四层。
七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
五层划分为:应用层、传输层、网络层、数据链路层、物理层。
四层划分为:应用层、传输层、网络层、网络接口层。
二、TCP和UDP协议区别
TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况;UDP 主要用于那些对高速传输
和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用。
- TCP:适用于对传输可靠性和顺序性要求高的应用场景,保证数据不丢失、不乱序,并提供流量控制和拥塞控制。
- UDP:适用于实时性要求高的应用,数据不必完全可靠传输,能容忍部分丢包,传输效率高。
2.1TCP 和 UDP 的主要区别
2.2 TCP协议特点
- 面向连接:TCP 是面向连接的协议,通信双方在传输数据前必须建立一个连接,这个过程称为"三次握手"。
- 可靠传输:TCP 提供可靠的数据传输服务,数据包的丢失、重复或顺序错误都会被检测并进行纠正。TCP使用确认机制,接收方必须确认收到的数据包。
- 数据流控制:TCP 具有流量控制和拥塞控制机制,通过滑动窗口和慢启动等算法,确保网络资源不会被超载。
- 有序传输:TCP 保证接收方按顺序接收到数据包,即使数据包乱序到达,TCP 也会重新排序。
- 报文段:TCP 将数据划分为大小适当的报文段,每个报文段都包含一个序列号,用于顺序重组数据。
- 应用场景:适用于对数据传输可靠性要求较高的场景,如文件传输(FTP)、电子邮件(SMTP)、网页浏览(HTTP/HTTPS)等。
2. 3 UDP协议特点
- 无连接:UDP 是无连接的协议,发送数据前不需要建立连接,也不会维持连接状态。发送方可以直接将数据报发送给接收方。
- 不可靠传输:UDP 不保证数据能可靠地送达接收方,不进行数据重传、丢包检测、重排序等操作。数据包可能会丢失、重复或乱序到达。
- 没有流量控制:UDP 没有流量控制和拥塞控制机制,适用于对实时性要求高、对丢包敏感度较低的应用。
- 数据报文:UDP 以数据报的形式发送,每个数据报是独立的,报文的大小不能超过 64KB。
- 应用场景:适用于对实时性要求较高,传输效率重要、容忍一定丢包的应用场景,如视频会议、语音通信(VoIP)、实时游戏、DNS查询等。
2.4 TCP建立连接的三次握手
TCP是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次"对话"才能建立起
来,其中的过程非常复杂, 只简单的描述下这三次对话的简单过
程:
1)主机A向主机B发出连接请求:"我想给你发数据,可以吗?",这是第一次对话;
2)主机B向主机A发送同意连接和要求同步 (同步就是两台主机一个在发送,一个在接收,协调工作)的数据包 :"可以,你什么时候发?",这是第二次对话;
3)主机A再发出一个数据包确认主机B的要求同步:"我现在就发,你接着吧!", 这是第三次握手。三次"对话"的目的是使数据包的发送和接收同步, 经过三次"对话"
之后,主机A才向主机B正式发送数据。
三次"对话"的目的是使数据包的发送和接收同步, 经过三次"对话"之后,主机A才向主机B正式发送数据。
第一步,客户端发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。
第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgement)
第三步,客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成。然后才开始通信的第二步:数据处理。
为什么TCP协议有三次握手,而UDP协议没有?
因为三次握手的目的是在client端和server端建立可靠的连接。保证双方发送的数据对方都能接受到,这也是TCP协议的被称为可靠的数据传输协议的原因。而UDP就不一样,UDP不提供可靠的传输模式,发送端并不需要得到接收端的状态,因此UDP协议就用不着使用三次握手。
2.5 TCP断开连接的四次挥手
TCP建立连接要进行3次握手,而断开连接要进行4次:
第一次: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;
第二次: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;
第三次: 由B 端再提出反方向的关闭请求,将FIN置1 ;
第四次: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.。
由TCP的三次握手和四次断开可以看出,TCP使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据
正式传输前就有了交互, 为数据正式传输打下了可靠的基础。
三、基于TCP协议的socket套接字编程
3.1 什么是scoket?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
3.2 套接字发展史及分类
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为"伯克利套接字"或"BSD 套接字"。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
3.2.1 基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
3.2.2 基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
3.3 套接字工作流程
一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束,使用以下Python代码实现:
python
import socket
# socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0
socket.socket(socket_family, socket_type, protocal=0)
# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能大幅减短我们的代码
tcpSock = socket(AF_INET, SOCK_STREAM)
3.3.1服务端套接字函数
3.3.2客户端套接字函数
3.3.3公共用途的套接字函数
3.3.4面向锁的套接字方法
3.3.5面向文件的套接字的函数
3.4 基于TCP协议的套接字编程(循环)
3.4.1服务端
python
import socket
#1、买手机
phone = socket.socket(socket.AF_INET,
socket.SOCK_STREAM) #tcp称为流式协议,udp称为数据报协议SOCK_DGRAM
# print(phone)
#2、插入/绑定手机卡
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1', 8080))
#3、开机
phone.listen(5) # 半连接池,限制的是请求数
#4、等待电话连接
print('start....')
while True: # 连接循环
conn, client_addr = phone.accept() #(三次握手建立的双向连接,(客户端的ip,端口))
# print(conn)
print('已经有一个连接建立成功', client_addr)
#5、通信:收\发消息
while True: # 通信循环
try:
print('服务端正在收数据...')
data = conn.recv(1024) #最大接收的字节数,没有数据会在原地一直等待收,即发送者发送的数据量必须>0bytes
# print('===>')
if len(data) == 0: break #在客户端单方面断开连接,服务端才会出现收空数据的情况
print('来自客户端的数据', data)
conn.send(data.upper())
except ConnectionResetError:
break
#6、挂掉电话连接
conn.close()
#7、关机
phone.close()
3.4.2客户端
python
import socket
#1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# print(phone)
#2、拨电话
phone.connect(('127.0.0.1', 8080)) # 指定服务端ip和端口
#3、通信:发\收消息
while True: # 通信循环
msg = input('>>: ').strip() #msg=''
if len(msg) == 0: continue
phone.send(msg.encode('utf-8'))
# print('has send----->')
data = phone.recv(1024)
# print('has recv----->')
print(data)
#4、关闭
phone.close()
3.5 模拟ssh远程执行命令
3.5.1 服务端
python
from socket import *
import subprocess
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
print('start...')
while True:
conn, client_addr = server.accept()
while True:
print('from client:', client_addr)
cmd = conn.recv(1024)
if len(cmd) == 0: break
print('cmd:', cmd)
obj = subprocess.Popen(cmd.decode('utf8'), # 输入的cmd命令
shell=True, # 通过shell运行
stderr=subprocess.PIPE, # 把错误输出放入管道,以便打印
stdout=subprocess.PIPE) # 把正确输出放入管道,以便打印
stdout = obj.stdout.read() # 打印正确输出
stderr = obj.stderr.read() # 打印错误输出
conn.send(stdout)
conn.send(stderr)
conn.close()
server.close()
-
conn.recv(1024)
:从客户端接收最多 1024 字节的数据。recv()
方法会阻塞,直到接收到数据。接收的是客户端发来的命令。- 如果接收到的
cmd
长度为 0,则表示客户端关闭连接,跳出循环。
- 如果接收到的
-
subprocess.Popen()
:创建一个子进程来执行客户端发来的命令。cmd.decode('utf8')
:将接收到的命令解码成字符串,recv()
接收到的是字节数据。shell=True
:表示通过 shell 执行命令。stderr=subprocess.PIPE
和stdout=subprocess.PIPE
:将标准输出和标准错误的输出重定向到管道,以便稍后读取。
-
stdout = obj.stdout.read()
:读取子进程标准输出的内容。 -
stderr = obj.stderr.read()
:读取子进程标准错误的内容。 -
conn.send(stdout)
和conn.send(stderr)
:将命令的执行结果或错误信息通过conn
发送回客户端。
3.5.2客户端
python
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
data = input('please enter your data')
client.send(data.encode('utf8'))
data = client.recv(1024)
print('from server:', data)
client.close()
3.6 聊天室编程
3.6.1服务端
python
import socket
import threading
def handle_client(client_socket, client_address):
print(f"{client_address} has connected.")
while True:
try:
message = client_socket.recv(1024).decode('utf-8')
if not message:
break
print(f"{client_address}: {message}")
broadcast(message, client_socket)
except ConnectionResetError:
break
print(f"{client_address} has disconnected.")
client_socket.close()
def broadcast(message, client_socket):
for client in clients:
if client != client_socket:
client.send(message.encode('utf-8'))
if __name__ == "__main__":
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
print("Server is running...")
clients = []
while True:
client_socket, client_address = server.accept()
clients.append(client_socket)
client_handler = threading.Thread(target=handle_client, args=(client_socket, client_address))
client_handler.start()
3.6.2客户端
python
import socket
import threading
def receive_messages(client_socket):
while True:
try:
message = client_socket.recv(1024).decode('utf-8')
if not message:
break
print(message)
except ConnectionResetError:
break
def main():
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 8080))
thread = threading.Thread(target=receive_messages, args=(client_socket,))
thread.start()
while True:
message = input()
if message.lower() == 'exit':
break
client_socket.send(message.encode('utf-8'))
client_socket.close()
if __name__ == "__main__":
main()
四、UDP编程介绍 、
UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。创建Socket时, SOCK_DGRAM 指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用 listen() 方法,而是直接接收来自任何客户端的数据。 recvfrom() 方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用 sendto() 就可以把数据用UDP发给客户端。
4.1 DUP编程的实现
服务端
python
#coding=utf-8
from socket import *
#最简化的UDP服务端代码
s = socket(AF_INET,SOCK_DGRAM) #创建UDP类型的套接字
s.bind(("127.0.0.1",8888)) #绑定端口,ip可以不写
print("等待接收数据!")
recv_data = s.recvfrom(1024) #1024表示本次接收的最大字节数
print(f"收到远程信息:{recv_data[0].decode('gbk')},from {recv_data[1]}")
s.close()
客户端
python
#coding=utf-8
from socket import *
#最简化的UDP客户端发送消息代码
s = socket(AF_INET,SOCK_DGRAM) #创建UDP类型的套接字
addr = ("127.0.0.1",8888)
data = input("请输入:")
s.sendto(data.encode("gbk"),addr)
s.close()
4.2 UDP持续接受信息
服务端
python
#coding=utf-8
from socket import *
#最简化的UDP服务端代码
s = socket(AF_INET,SOCK_DGRAM) #创建UDP类型的套接字
s.bind(("127.0.0.1",8888)) #绑定端口,ip可以不写
print("等待接收数据!")
while True:
recv_data = s.recvfrom(1024) #1024表示本次接收的最大字节数
recv_content = recv_data[0].decode('gbk')
print(f"收到远程信息:{recv_content},from {recv_data[1]}")
if recv_content == "88":
print("结束聊天!")
break
s.close()
客户端
python
#coding=utf-8
from socket import *
#最简化的UDP客户端发送消息代码
s = socket(AF_INET,SOCK_DGRAM) #创建UDP类型的套接字
addr = ("127.0.0.1",8888)
while True:
data = input("请输入:")
s.sendto(data.encode("gbk"),addr)
if data == "88":
print("结束聊天!")
break
s.close()
参考文献
电脑间数据通信-OSI协议简述版 - B站-水论文的程序猿 - 博客园
TCP协议的三次握手和四次挥手 - B站-水论文的程序猿 - 博客园
基于TCP协议的socket套接字编程 - B站-水论文的程序猿 - 博客园
Socket抽象层 - B站-水论文的程序猿 - 博客园
模拟ssh远程执行命令 - B站-水论文的程序猿 - 博客园