网络编程
一. 网络架构
1.1 CS架构
应用领域:
计算机发展初期用户去取数据,直接就去主机拿,从这里开始就分出了客户端和服务端。
客户端:用户安装的软件;
服务端:统一管理数据库的主机中的软件就叫做服务端,再后来服务端不只是管理数据,外加处理业务逻辑。
1.1.1CS架构要求
1.用户操作系统安装客户端;产商操作系统部署服务端
2.每个用户需要独立安装软件、服务端升级也要每个用户升级
1.1.2 面试题:数据放在服务端和客服端的利与弊?
服务端统一处理有更好的安全性和稳定性而且升级比较容易,不过服务器负担就增加了。
客服端将负担分配到每一个用户,从而可以节约服务器资源,安全性和稳定性可能会有一定的问题,但是升级比较麻烦,每个安装的客户端程序需要升级,另外为了节省网络资源,通过网络传输的数据应该尽量减少。
1.2 BS架构
1.2.1 两种BS架构
1.3. CS架构和BS架构区别
二. osi七层协议
互联网的本质就是一系列的网络协议,这个协议就叫OSI协议(一系列协议),按照功能不同,分工不同,人为的分层七层。实际上这个七层是不存在的。没有这七层的概念,只是人为的划分而已。区分出来的目的只是让你明白哪一层是干什么用的。
每一层都运行不同的协议。协议是干什么的,协议就是标准。
实际上还有人把它划成五层、四层。
七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
五层划分为:应用层、传输层、网络层、数据链路层、物理层。
四层划分为:应用层、传输层、网络层、网络接口层。
每层运行常见的物理设备
2.1 TCP三次握手和四次挥手
三. socket层
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
四. 套接字工作流程
服务器端:服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。如果客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客服端与服务端的连接就建立了。客服端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客服端读取数据,最后关闭连接,依次交互结束。
import socket
#socket.socket(socket_family,socket_type,protocal=0) socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 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)
服务端套接字函数
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() 关闭套接字
面向锁的套接字方法 s.setblocking() 设置套接字的阻塞与非阻塞模式 s.settimeout() 设置阻塞套接字操作的超时时间 s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数 s.fileno() 套接字的文件描述符 s.makefile() 创建一个与该套接字相关的文件
五. 基于TCP的套接字
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
#tcp服务端
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字
ss.listen() #监听链接
inf_loop:
cs = ss.accept() #接受客户端链接
comm_loop:
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关客户端套接字
ss.close() #关闭服务器套接字(可选)
#tcp客户端
cs = socket() #创建客户套接字
cs.connect() #尝试连接服务器
comm_loop:
cs.send()/cs.recv() #对话(发送/接收)
cs.close() #关闭客户套接字
实例:
#服务端
import socket
#1、买手机(创建服务器套接字)
server = socket.sockket(socket.AF_INET,socket.SOCK_STREAM) #tcp称为流式协议,udp称为数据报协议SOCK_DGRAM
#2、插入/绑定手机卡(把地址绑定到套接字)
server.bind(('127.0.0.1',8081))
#3、开机(#监听链接)
server.listen(5) # 半连接池,限制的是请求数
#4、等待电话连接
print('start......')
conn, client_addr = server.accept() #接受客户端链接
#5、通信:收\发消息
data = conn.recv(1024)
print('来自客服端的数据:', data) #最大接收的字节数
conn.send(data.upper())
#6、挂掉电话连接(#关客户端套接字)
conn.close()
#7、关机(关闭服务器套接字(可选))
server.close()
#客服端
import socket
#1、买手机(创建客户套接字)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#2、拨电话(尝试连接服务器)
client.connect(('127.0.0.1',8081))
#3、通信:发\收消息
client.send('hello'.encode('utf-8'))
data = client.recv(1024)
print(data)
#4、关闭
client.close()
基于TCP协议的套接字编程(循环)
#服务端
import socket
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp称为流式协议,udp称为数据报协议SOCK_DGRAM
server.bind(('127.0.0.1', 8080))
server.listen(5)
print('start...')
while True: #连接循环
conn, client_addr = server.accept() #建立双向链接
print('已经有一个连接建立成功',client_addr)
while True: #通信循环
try:
print('服务端正在收数据...')
data = conn.recv(1024) #最大接收的字节数,没有数据会在原地一直等待收,即发送者发送的数据量必须>0bytes
if len(data) == 0: #在客户端单方面断开连接,服务端才会出现收空数据的情况
break
print('来自客户端的数据', data)
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
server.close()
#客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True: #通信循环
msg = input('>>>:').strip()
client.send(msg.encode('utf-8'))
data = client.send(1024)
print(data)
六. 模拟ssh远程执行命令
#服务端
from socket import *
import subpricess
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:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'), #输入的cmd命令
shell = Ture, #通过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()
#客户端
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').strip()
if len(data) == 0:
break
client.send(data.encode('utf-8'))
data = client.recv(1024)
print('from server:', data.decode('utf-8'), end='')
client.close()
上述程序是基于tcp的socket,在运行时会发生粘包
七. 基于UDP的套接字
udp是无链接的,先启动哪一端都不会报错
#udp服务端
ss = socket() #创建一个服务器的套接字
ss.bind() #绑定服务器套接字
inf_loop:
cs = ss.recvfrom()/sendto() #接收与发送
ss.close()
#udp客户端
cs = socket() #创建客户套接字
comm_loop:
cs.sendto()/cs.recvfrom() #发送/接收
cs.close()
#服务端
import socket
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)
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()
client.sendto(msg.encode('utf-8'))
data, server_addr = client.recvfrom(1024)
print(data)
client.close()
1、udp是无链接的,先启动哪一端都不会报错
2、udp协议是数据报协议,发空的时候也会自带报头,因此客户输入空,服务端也能收到
八. 粘包
8.1 什么是粘包
须知:只有TCP有粘包现象,UDP永远不会粘包。
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化
-
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
-
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
-
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
两种情况下会发生粘包。
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
8.2 解决粘包问题
low版
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
#服务端
import socket, subprocess
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, addr = server.accept()
print('atart......')
while True:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
stdout = obj.stdout.read()
if stdout:
ret = stdout
else:
stderr = obj.stderr.read()
ret = stderr
ret_len = len(ret)
conn.send(str(ret_len).encode('utf-8'))
data = conn.recv(1024).decode('utf-8')
if data == 'recv_ready':
conn.sendall(ret)
conn.close()
server.close()
#客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
msg = input('please enter your cmd you want>>>').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf-8'))
length = int(client.recv(1024))
client.send('recv_ready'.encode('utf-8'))
send_size = 0
recv_size = 0
data = 'b'
while recv_size < length:
data = client.recv(1024)
recv_size += len(data)
print(data.decode('utf-8'))
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
8.3 struct模块解决粘包问题
8.3.1 struct模块的使用
解决粘包问题的核心就是:为字节流加自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
##############使用struct模块创建报头
import json
import struct
#为避免粘包,必须自定制报头
header={'file_size':1073741824000,
'file_name':'/a/b/c/d/e/a.txt',
'md5':'8f6fbf8347faa4924a76856701edb0f3'
} #1T数据,文件路径和md5值
#为了该报头能传送,需要序列化并且转为bytes
head_bytes = bytes(json.dunmps(header_dic),encoding='utf-8')
#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度
#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式
#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度
head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头
#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
#############服务端
from socket import *
import subprocess
import struct
import json
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
print('start...')
while True:
conn, client_addr = server.accept()
print(conn, client_addr)
while True:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
stderr = obj.stderr.read()
stdout = obj.stdout.read()
# 制作报头
header_dict = {
'filename': 'a.txt',
'total_size': len(stdout) + len(stderr),
'hash': 'xasf123213123'
}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode('utf8')
# 1. 先把报头的长度len(header_bytes)打包成4个bytes,然后发送
conn.send(struct.pack('i', len(header_bytes)))
# 2. 发送报头
conn.send(header_bytes)
# 3. 发送真实的数据
conn.send(stdout)
conn.send(stderr)
conn.close()
server.close()
####################客户端
from socket import *
import json
import struct
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
while True:
cmd = input('please enter your cmd you want>>>')
if len(cmd) == 0: continue
client.send(cmd.encode('utf8'))
# 1. 先收4个字节,这4个字节中包含报头的长度
header_len = struct.unpack('i', client.recv(4))[0]
# 2. 再接收报头
header_bytes = client.recv(header_len)
# 3. 从包头中解析出想要的东西
header_json = header_bytes.decode('utf8')
header_dict = json.loads(header_json)
total_size = header_dict['total_size']
# 4. 再收真实的数据
recv_size = 0
res = b''
while recv_size < total_size:
data = client.recv(1024)
res += data
recv_size += len(data)
print(res.decode('utf8'))
client.close()