python-网络编程
1. 网络编程
1.1 网络基础知识
1.1.1 什么是网络
-
什么是网络 : 计算机网络功能主要包括实现资源共享,实现数据信息的快速传递。


1.1.2 网络通信标准
-
面临问题
- 不同的国家和公司都建立自己的通信标准不利于网络互连
- 多种标准并行情况下不利于技术的发展融合

-
OSI 7层模型

-
好处
-
建立了统一的通信标准
-
降低开发难度,每层功能明确,各司其职
-
七层模型实际规定了每一层的任务,该完成什么事情
-
-
-
TCP/IP模型
- 七层模型过于理想,结构细节太复杂
- 在工程中应用实践难度大
- 实际工作中以TCP/IP模型为工作标准流程

-
网络协议
-
什么是网络协议:在网络数据传输中,都遵循的执行规则。
-
网络协议实际上规定了每一层在完成自己的任务时应该遵循什么规范。
-
-
需要应用工程师做的工作 : 编写应用工功能,明确对方地址,选择传输服务。

1.1.3 通信地址
-
IP地址
-
IP地址 : 即在网络中标识一台计算机的地址编号。
-
IP地址分类
- IPv4 : 192.168.1.5
- IPv6 :fe80::80a:76cf:ab11:2d73
-
IPv4 特点
- 分为4个部分,每部分是一个整数,取值分为0-255
-
IPv6 特点(了解)
- 分为8个部分,每部分4个16进制数,如果出现连续的数字 0 则可以用 ::省略中间的0
-
IP地址相关命令
-
ifconfig : 查看Linux系统下计算机的IP地址

-
ping [ip]:查看计算机的连通性

-
-
公网IP和内网IP
- 公网IP指的是连接到互联网上的公共IP地址,大家都可以访问。(将来进公司,公司会申请公网IP作为网络项目的被访问地址)
- 内网IP指的是一个局域网络范围内由网络设备分配的IP地址。
-
-
端口号
-
端口:网络地址的一部分,在一台计算机上,每个网络程序对应一个端口。

-
端口号特点
- 取值范围: 0 —— 65535 的整数
- 一台计算机上的网络应用所使用的端口不会重复
- 通常 0——1023 的端口会被一些有名的程序或者系统服务占用,个人一般使用 > 1024的端口
-
1.1.4 服务端与客户端
-
服务端(Server):服务端是为客户端服务的,服务的内容诸如向客户端提供资源,保存客户端数据,处理客户端请求等。
-
客户端(Client) :也称为用户端,是指与服务端相对应,为客户提供一定应用功能的程序,我们平时使用的手机或者电脑上的程序基本都是客户端程序。

1.2 UDP 传输方法
1.2.1 套接字简介
-
套接字(Socket) : 实现网络编程进行数据传输的一种技术手段,网络上各种各样的网络服务大多都是基于 Socket 来完成通信的。

-
Python套接字编程模块:import socket
1.2.3 UDP套接字编程
- 创建套接字
sock=socket.socket(family,type)
功能:创建套接字
参数:family 网络地址类型 AF_INET表示ipv4
type 套接字类型 SOCK_DGRAM 表示udp套接字 (也叫数据报套接字)
返回值: 套接字对象
- 绑定地址
- 本地地址 : ‘localhost’ , ‘127.0.0.1’
- 网络地址 : ‘172.40.91.185’ (通过ifconfig查看)
- 自动获取地址: ‘0.0.0.0’

sock.bind(addr)
功能: 绑定本机网络地址
参数: 二元元组 (ip,port) ('0.0.0.0',8888)
- 消息收发
data,addr = sock.recvfrom(buffersize)
功能: 接收UDP消息
参数: 每次最多接收多少字节
返回值: data 接收到的内容
addr 消息发送方地址
n = sock.sendto(data,addr)
功能: 发送UDP消息
参数: data 发送的内容 bytes格式
addr 目标地址
返回值:发送的字节数
- 关闭套接字
sock.close()
功能:关闭套接字
UDP服务端代码示例:
"""
udp_server.py
udp服务端实例代码
重点代码 !!!
"""
from socket import *
# 创建UDP套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)
# 绑定地址
udp_socket.bind(("0.0.0.0",8888))
while True:
# 接收发送消息 data--> bytes
data,addr = udp_socket.recvfrom(5)
# if data == b"##":
# break
print("从",addr,"收到:",data.decode())
# 发送给刚才收到的地址
udp_socket.sendto(b"Thanks",addr)
# 关闭套接字
udp_socket.close()
UDP 客户端代码示例:
"""
udp_client.py
udp 客户端示例
重点代码!!
"""
from socket import *
# 服务器地址
ADDR = ("127.0.0.1",8888)
# 与服务端相同套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)
# 发送消息
while True:
msg = input(">>")
if not msg:
break
udp_socket.sendto(msg.encode(),ADDR)
# 结束发送
# if msg == "##":
# break
data,addr = udp_socket.recvfrom(128)
print("从服务端收到:",data.decode())
udp_socket.close()
-
服务端客户端流程

随堂练习:
使用udp完成网络单词查询
从客户端输入单词,发送给服务端,得到单词的解释,打印出来
利用 dict 数据库下的 words表来完成
########################## 服务端 ###############################
from socket import *
import pymysql
# 数据处理类
class Dict:
def __init__(self):
self.kwargs = {
"host": "localhost",
"port": 3306,
"user": "root",
"password": "123456",
"database": "dict",
"charset": "utf8"
}
self.connect()
# 完成数据库连接
def connect(self):
self.db = pymysql.connect(**self.kwargs)
self.cur = self.db.cursor()
# 关闭
def close(self):
self.cur.close()
self.db.close()
def get_mean(self, word):
sql = "select mean from words where word=%s;"
self.cur.execute(sql, [word])
mean = self.cur.fetchone() # (mean,) None
if mean:
return mean[0]
else:
return "Not Found"
# 逻辑处理 网络搭建
class QueryWord:
def __init__(self, host="0.0.0.0", port=8888):
self.host = host
self.port = port
self.dict = Dict()
self.sock = self.create_socket()
def create_socket(self):
sock = socket(AF_INET, SOCK_DGRAM)
sock.bind((self.host, self.port))
return sock
def close(self):
self.sock.close()
# 查找单词方法
def query_word(self):
while True:
word, addr = self.sock.recvfrom(128)
# 查询单词
mean = self.dict.get_mean(word.decode())
self.sock.sendto(mean.encode(), addr)
if __name__ == '__main__':
query = QueryWord()
query.query_word()
############################ 客户端代码#########################
from socket import *
# 服务器地址
ADDR = ("127.0.0.1",8888)
class QueryWord:
def __init__(self):
self.sock = socket(type=SOCK_DGRAM)
def close(self):
self.sock.close()
# 网络传输
def recv_mean(self,word):
self.sock.sendto(word.encode(),ADDR)
mean,addr = self.sock.recvfrom(1024)
return mean.decode()
# 输入输出
def query_word(self):
while True:
word = input("Word:")
if not word:
break
mean = self.recv_mean(word)
print("%s : %s"%(word,mean))
if __name__ == '__main__':
query = QueryWord()
query.query_word() # 查单词
query.close()
1.2.4 UDP套接字特点
- 可能会出现数据丢失的情况
- 传输过程简单,实现容易
- 数据以数据包形式表达传输
- 数据传输效率较高
1.3 TCP 传输方法
1.3.1 TCP传输特点
-
面向连接的传输服务
- 传输特征 : 提供了可靠的数据传输,可靠性指数据传输过程中无丢失,无失序,无差错,无重复。
- 可靠性保障机制(都是操作系统网络服务自动帮应用完成的):
- 在通信前需要建立数据连接
- 确认应答机制
- 通信结束要正常断开连接
-
三次握手(建立连接)
- 客户端向服务器发送消息报文请求连接
- 服务器收到请求后,回复报文确定可以连接
- 客户端收到回复,发送最终报文连接建立

- 四次挥手(断开连接)
- 主动方发送报文请求断开连接
- 被动方收到请求后,立即回复,表示准备断开
- 被动方准备就绪,再次发送报文表示可以断开
- 主动方收到确定,发送最终报文完成断开

1.3.2 TCP服务端

- 创建套接字
sock=socket.socket(family,type)
功能:创建套接字
参数:family 网络地址类型 AF_INET表示ipv4
type 套接字类型 SOCK_STREAM 表示tcp套接字 (也叫流式套接字)
返回值: 套接字对象
- 绑定地址 (与udp套接字相同)
- 设置监听
sock.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小

- 处理客户端连接请求
conn,addr = sockfd.accept()
功能: 阻塞等待处理客户端请求
返回值: conn 客户端连接套接字
addr 连接的客户端地址
- 消息收发
data = conn.recv(buffersize)
功能 : 接受客户端消息
参数 :每次最多接收消息的大小
返回值: 接收到的内容
n = conn.send(data)
功能 : 发送消息
参数 :要发送的内容 bytes格式
返回值: 发送的字节数
- 关闭套接字 (与udp套接字相同)
TCP服务端示例01:
"""
TCP服务端函数示例
重点代码 !!
"""
from socket import *
# 创建tcp套接字
tcp_socket = socket(AF_INET,SOCK_STREAM)
# 绑定地址
tcp_socket.bind(("0.0.0.0",8888))
# 设置为监听套接字
tcp_socket.listen(5)
# 等待客户端连接
while True:
print("Waiting for connect...")
connfd,addr = tcp_socket.accept()
print("Connect from",addr)
# 循环收发消息 客户端退出 recv立即返回b""
while True:
data = connfd.recv(5)
# data=b""客户端直接关闭 b"##"客户端主动告知关闭
if not data or data == b'##':
break
print("收到:",data.decode())
connfd.send(b"Thanks/")
connfd.close()
# 关闭套接字
tcp_socket.close()
1.3.3 TCP客户端

- 创建TCP套接字
- 请求连接
sock.connect(server_addr)
功能:连接服务器
参数:元组 服务器地址
- 收发消息
注意: 防止两端都阻塞,recv send要配合
-
关闭套接字
TCP客户端示例01: """ TCP套接字编程 客户端 重点代码 !! """ from socket import * # 服务端地址 ADDR = ("127.0.0.1",8888) # 使用默认值--》tcp tcp_socket = socket() # 发起连接 tcp_socket.connect(ADDR) # 循环发送接收消息 while True: msg = input(">>") tcp_socket.send(msg.encode()) # 结束发送 if msg == "##": break data = tcp_socket.recv(1024) print("From server:",data.decode()) tcp_socket.close()

练习:
在客户端将一张图片上传到服务端,图片自选,上传到服务端后命名为 recv.jpg
思路: 客户端 获取文件内容--》发送出去
服务端 接收文件内容--》写入磁盘
########################## 服务端部分 #####################
from socket import *
# 接收内容,写入文件
def recv_file(connfd):
file = open("recv.jpg",'wb')
# 边收边写
while True:
data = connfd.recv(1024)
if data == b"##":
break
file.write(data)
file.close()
connfd.send("上传完毕".encode())
def main():
# 创建tcp套接字
tcp_socket = socket()
tcp_socket.bind(("0.0.0.0",8888))
tcp_socket.listen(5)
while True:
connfd,addr = tcp_socket.accept()
print("Connect from",addr)
# 接收文件
recv_file(connfd)
connfd.close()
if __name__ == '__main__':
main()
############################ 客户端部分 ###################################
from socket import *
def send_file(tcp_socket,filename):
file = open(filename,"rb")
# 边读边发送
while True:
data = file.read(1024)
if not data:
break
tcp_socket.send(data)
file.close()
tcp_socket.send(b"##") # 告知服务端结束
msg = tcp_socket.recv(128)
print(msg.decode())
def main(filename):
tcp_socket = socket()
tcp_socket.connect(("127.0.0.1",8888))
# 发送文件
send_file(tcp_socket,filename)
tcp_socket.close()
if __name__ == '__main__':
main("./zly.jfif")
TCP服务端示例02:
from socket import *
# 创建tcp套接字
tcp_socket = socket(AF_INET,SOCK_STREAM)
# 绑定地址
tcp_socket.bind(("0.0.0.0",8880))
# 设置为监听套接字
tcp_socket.listen(5)
# 循环连接 收发 断开 -》 每次发送消息都需要连接
while True:
connfd,addr = tcp_socket.accept()
data = connfd.recv(1024)
print("收到:",data.decode())
connfd.send(b"Thanks")
connfd.close()
# 关闭套接字
tcp_socket.close()
TCP客户端示例02:
from socket import *
# 服务端地址
ADDR = ("127.0.0.1",8880)
# 循环发送接收消息
while True:
msg = input(">>")
if not msg:
break
tcp_socket = socket()
tcp_socket.connect(ADDR)
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("From server:",data.decode())
tcp_socket.close()
1.3.4 TCP套接字细节
-
tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
-
tcp连接中如果一端已经不存在,仍然试图通过send向其发送数据则会产生BrokenPipeError
-
一个服务端可以同时连接多个客户端,也能够重复被连接
-
tcp粘包问题
-
产生原因
- 为了解决数据再传输过程中可能产生的速度不协调问题,操作系统设置了缓冲区
- 实际网络工作过程比较复杂,导致消息收发速度不一致
- tcp以字节流方式进行数据传输,在接收时不区分消息边界

-
带来的影响
- 如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。
-
处理方法
- 消息格式化处理,如人为的添加消息边界,用作消息之间的分割
- 控制发送的速度
-
随堂练习:
在客户端有一些数据
data = [
"张三 18 177",
"李四 19 180",
"王五 120 183"
]
从客户端向服务端发送这些数据,在服务端将这些数据分别写入到一个文件中,每个数据占一行
客户端发送完成是 向服务端发送 '#' 表示发送完毕
######################### 服务端 ######################
from socket import *
# 接收数据 (对应客户端方法1)
# def recv_data(connfd):
# file = open("student.txt",'w')
# while True:
# data = connfd.recv(1024).decode()
# if data == '##':
# break
# file.write(data + '\n')
# file.close()
# 接收数据 (对应客户端方法2)
def recv_data(connfd):
file = open("student.txt", 'wb')
# data--> xxxx\nxxxx\nxxxx\n
data = connfd.recv(1024 * 1024)
file.write(data)
file.close()
def main():
sock = socket()
sock.bind(("0.0.0.0",8888))
sock.listen(3)
connfd,addr = sock.accept()
print("连接:",addr)
recv_data(connfd) # 接收数据
if __name__ == '__main__':
main()
###################### 客户端 ################################
from socket import *
from time import sleep
data = [
"张三 18 177",
"李四 19 180",
"王五 120 183"
]
# 发送数据 (处理粘包方法1)
# def send_data(sock):
# for item in data:
# sock.send(item.encode())
# sleep(0.1) # 延迟发送
# sock.send(b"##") # 表示发送完成
# 发送数据 (处理粘包方法2)
def send_data(sock):
info = '\n'.join(data)
sock.send(info.encode()) # 一次性发送
def main():
sock = socket()
sock.connect(("127.0.0.1",8888))
send_data(sock) # 发送数据
sock.close()
if __name__ == '__main__':
main()
1.3.5 TCP与UDP对比
-
传输特征
- TCP提供可靠的数据传输,但是UDP则不保证传输的可靠性
- TCP传输数据处理为字节流,而UDP处理为数据包形式
- TCP传输需要建立连接才能进行数据传,效率相对较低,UDP比较自由,无需连接,效率较高
-
套接字编程区别
- 创建的套接字类型不同
- tcp套接字会有粘包,udp套接字有消息边界不会粘包
- tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要
- tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom
-
使用场景
- tcp更适合对准确性要求高,传输数据较大的场景
- 文件传输:如下载电影,访问网页,上传照片
- 邮件收发
- 点对点数据传输:如点对点聊天,登录请求,远程访问,发红包
- udp更适合对可靠性要求没有那么高,传输方式比较自由的场景
- 视频流的传输: 如部分直播,视频聊天等
- 广播:如网络广播,群发消息
- 实时传输:如游戏画面
- 在一个大型的项目中,可能既涉及到TCP网络又有UDP网络
- tcp更适合对准确性要求高,传输数据较大的场景
随堂练习:
完成一个对话小程序,客户端可以发送问题给服务端,服务端接收到问题将对应答案给客户端,客户端打印出来
要求可以同时多个客户端提问,如果问题没有指定答案,则回答 “人家还小,不知道。”
注意: 不需要使用数据库文件存储应答内容,在服务端用字典表示关键字和答案之间的对应关系即可
{"key":"value"}
key: 几岁
value : 我2岁啦
################ 服务端 ############################
from socket import *
# 对话字典
chat = {
"你好":"你好啊!",
"叫什么":"我叫小美",
"男生女生":"我是机器人啦",
"你几岁":"我2岁啦"
}
def handle(connfd):
# q 客户端问题
q = connfd.recv(1024).decode()
for key,value in chat.items():
if key in q:
connfd.send(value.encode())
break
else:
connfd.send("人家还小不知道啦。".encode())
def main():
sock = socket()
sock.bind(("0.0.0.0",8888))
sock.listen(5)
# 循环处理对话
while True:
connfd,addr = sock.accept()
handle(connfd) # 接收问题回答问题
connfd.close()
if __name__ == '__main__':
main()
####################### 客户端 ###############################
from socket import *
# 服务器地址
ADDR = ("127.0.0.1",8888)
def chat(msg):
sock = socket()
sock.connect(ADDR)
sock.send(msg.encode())
result = sock.recv(1024)
sock.close()
return result.decode()
# 创建套接字
def main():
while True:
msg = input("我:")
if not msg:
break
result = chat(msg)
print("小美:",result)
if __name__ == '__main__':
main()
1.4 数据传输过程
1.4.1 传输流程
- 发送端由应用程序发送消息,逐层添加首部信息,最终在物理层发送消息包。
- 发送的消息经过多个节点(交换机,路由器)传输,最终到达目标主机。
- 目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息。

1.4.2 TCP协议首部信息

-
源端口和目的端口 各占2个字节,分别写入源端口和目的端口。
-
序号 占4字节。TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。例如,一报文段的序号是301,而接待的数据共有100字节。这就表明本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。
-
确认号 占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
-
确认ACK(ACKnowledgment) 仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。
-
同步SYN(SYNchronization) 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1,因此SYN置为1就表示这是一个连接请求或连接接受报文。
-
终止FIN(FINis,意思是“完”“终”) 用来释放一个连接。当FIN=1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)