网络编程

网络编程

31、网络编程

一. 网络架构

1.1 CS架构

149-网络架构及其演变过程-cs架构.jpg?x-oss-process=style/watermark

应用领域:

计算机发展初期用户去取数据,直接就去主机拿,从这里开始就分出了客户端和服务端。

客户端:用户安装的软件;

服务端:统一管理数据库的主机中的软件就叫做服务端,再后来服务端不只是管理数据,外加处理业务逻辑。

1.1.1CS架构要求

1.用户操作系统安装客户端;产商操作系统部署服务端

2.每个用户需要独立安装软件、服务端升级也要每个用户升级

1.1.2 面试题:数据放在服务端和客服端的利与弊?

服务端统一处理有更好的安全性和稳定性而且升级比较容易,不过服务器负担就增加了。

客服端将负担分配到每一个用户,从而可以节约服务器资源,安全性和稳定性可能会有一定的问题,但是升级比较麻烦,每个安装的客户端程序需要升级,另外为了节省网络资源,通过网络传输的数据应该尽量减少。

1.2 BS架构

149-网络架构及其演变过程-bs架构.jpg?x-oss-process=style/watermark

1.2.1 两种BS架构

149-网络架构及其演变过程-osi和tcp.jpg?x-oss-process=style/watermark

1.3. CS架构和BS架构区别

149-网络架构及其演变过程-csbs区别.jpg?x-oss-process=style/watermark

二. osi七层协议

互联网的本质就是一系列的网络协议,这个协议就叫OSI协议(一系列协议),按照功能不同,分工不同,人为的分层七层。实际上这个七层是不存在的。没有这七层的概念,只是人为的划分而已。区分出来的目的只是让你明白哪一层是干什么用的。

每一层都运行不同的协议。协议是干什么的,协议就是标准。

实际上还有人把它划成五层、四层。

七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。

五层划分为:应用层、传输层、网络层、数据链路层、物理层。

四层划分为:应用层、传输层、网络层、网络接口层。

151-大白话OSI七层协议-七层协议.jpg?x-oss-process=style/watermark

每层运行常见的物理设备

151-大白话OSI七层协议-物理设备.jpg?x-oss-process=style/watermark

2.1 TCP三次握手和四次挥手

151-大白话OSI七层协议-tcp三次握手和四次挥手.jpg?x-oss-process=style/watermark

三. socket层

img

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

四. 套接字工作流程

img

服务器端:服务器端先初始化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永远不会粘包。

img

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

  3. 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模块的使用

124-解决粘包问题-struct模块参数.png?x-oss-process=style/watermark

解决粘包问题的核心就是:为字节流加自定义固定长度报头,报头中包含字节流长度,然后一次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()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2021-09-26 10:59  vonmo  阅读(131)  评论(0编辑  收藏  举报