python网络编程内容_Python网络编程

1. socket套接字

1.1 socket简介

1)网络中的进程间通信

在本地可以通过进程PID来唯一标识一个进程,但是在网络中这却是行不通的。

TCP/IP协议族已经解决了这个问题,网络层的“IP地址”可以唯一标识网络中的主机,而传输层“协议+端口”可以唯一标识主机中的应用程序(进程)。

这样利用IP地址,协议,端口 就可以表示网络中的进程了,网络中的进程通信就可以利用这个标志与其他进程进行交互。

2)什么是socket

socket简称套接字,是进程间通信的一种方式;它能实现不同主机间的进程间通信。

socket是应用层与TCP/IP协议族通信中间软件抽象层,它是一组接口。

在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议;进而,我们无需去关心TCP/UDP协议的细节,因为socket已经封装好了,我们只需要遵循socket的规定去编程,自然就是遵循tcp/udp标准的。

3)socket层

1278240-20200814133156036-19563667.png

1.2 套接字的分类

基于文件类型的套接字家族:AF_UNIX

在泛unix系统上,基于文件的套接字调用的就是底层的文件系统来取数据,

两个套接字进程运行在同一机器上,可以通过访问同一个文件来间接完成通信。

基于网络类型的套接字家族:AF_INET

还有AF_INET6被用于ipv6

1.3 套接字的工作流程

1)工作流程图示

1278240-20200814133836418-887844181.png

2)工作流程解释

服务器端:服务端先初始化socket,然后与端口绑定(bind),对端口进行监听(listen),再调用accept阻塞,等待客户端连接

客户端:服务端初始化完毕后,如果有个客户端初始化一个socket,然后连接服务器(connect),如果简连接成功,这时客户端与服务端的连接就建立了

客户端与服务端进行交互:客户端发送数据请求,服务端接受请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

1.4 创建socket

1)创建socket示例

socket_family:可以选择AF_INET或者AF_UNIX

socket_type:可以选择SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要用于UDP协议)

importsocket

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)

2)服务端套接字函数

s.bind() #绑定(主机,端口号)到套接字

s.listen() #开始TCP监听

s.accept() #被动接受TCP客户的连接,(阻塞式)等待连接的到来

3)客户端套接字函数

s.connect() #主动初始化TCP服务器连接

s.connect_ex() #connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

4)公共用途的套接字函数

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() #关闭套接字

5)面向锁的套接字方法

s.setblocking() #设置套接字的阻塞与非阻塞模式

s.settimeout() #设置阻塞套接字操作的超时时间

s.gettimeout() #得到阻塞套接字操作的超时时间

6)面向文件的套接字的函数

s.fileno() #套接字的文件描述符

s.makefile() #创建一个与该套接字相关的文件

2. 基于TCP的套接字

2.1 简单的tcp服务端&客户端

tcp是基于连接的,必须先启动服务端,然后再启动客户端去连接服务端。

1)socket服务端

importsocket#创建tcp套接字

phone =socket.socket(socket.AF_INET, socket.SOCK_STREAM)#绑定地址,这里接受的参数是一个元组的形式

phone.bind(('127.0.0.1', 8000))#监听连接

phone.listen(5)#此处的listen中的值表示处于半连接和已连接状态的client总和#等待中的半连接和已连接都保存在backlog中,此处的linsten的数量相当于就是在指定backlog的大小

# 如果当前已建立连接数和半连接数达到设定值,那么新客户端就不会connect成功,而是等待服务器

#接受客户端连接,在这个位置等待接收客户端发送的消息

conn, addr =phone.accept()#conn表示为这个客户端创建出了包含tcp三次握手信息的新的套接字#addr 包含这个客户端的信息

msg= conn.recv(1024) #接受客户端发来的消息

print('客户端发来的消息是:', msg)

conn.send(msg.upper())#将接受到的消息转换成大写的形式再发给客户端

conn.close()#触发四次挥手

phone.close() #关闭socket

2)socket客户端

importsocket

phone=socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.connect(('127.0.0.1', 8000)) #链接服务端的相应端口下的应用

phone.send('hello'.encode('utf-8')) #接受和发送消息时数据都应该是字节格式

data= phone.recv(1024)print('收到服务端发来的消息:', data)

phone.close()

2.2 循环收发消息

1)socket服务端(循环收发)

接收消息的的本质其实是在本机的内核空间内提取内容

发送消息的本质其实是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送

from socket import *tcp_server=socket(AF_INET, SOCK_STREAM)

ip_port= ('127.0.0.1', 9999)

listen_buffer= 5recv_buffer= 1024tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR,1) #解决端口重用的问题tcp_server.bind(ip_port)

tcp_server.listen(listen_buffer)whileTrue:print('服务端开始运行----->')

conn, addr=tcp_server.accept()whileTrue:try:print('conn----->',conn)print('addr----->',addr)

date= conn.recv(recv_buffer) #这里的收消息其实是在本机的内核空间内提取内容

print(date.decode('utf-8'))

conn.send('hello world!?'.encode('utf-8')) #发消息的本质是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送

#conn.sendall(data) # sendall函数可以将一整个数据循环着发送

except Exception: #捕获当客户端意外断开时的异常,若发生异常,则退出内循环,而重新尝试连接

breakconn.close()

tcp_server.close()

2)socket客户端(循环收发)

from socket import *tcp_client=socket(AF_INET, SOCK_STREAM)

ip_port= ('127.0.0.1', 9999)

buffer_size= 1024tcp_client.connect(ip_port)whileTrue:

msg= input("请输入要发送的内容:").strip()if not msg : continuetcp_client.send(msg.encode("utf-8")) #这里如果发送的消息为空(直接敲回车),则没有任何内容发往本机内核空间,所以消息根本就不会发送

data=tcp_client.recv(buffer_size)print("接收到的消息为:", data.decode('utf-8'))

tcp_client.close()

2.3 关于端口重用

1)关于Address already in use的报错

由于服务端仍然存在四次挥手的time_wait状态在占用地址

服务器高并发情况下会有大量的time_wait状态的优化方法

2)端口重用的解决方法一:添加socket配置

#加一条socket配置,重用ip和端口

phone=socket(AF_INET,SOCK_STREAM)

phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #在bind前加上这一句

phone.bind(('127.0.0.1',8080))

3)端口重用的解决方法二:修改内核参数

#发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,

vi /etc/sysctl.conf#编辑文件,加入以下内容:

net.ipv4.tcp_syncookies = 1net.ipv4.tcp_tw_reuse= 1net.ipv4.tcp_tw_recycle= 1net.ipv4.tcp_fin_timeout= 30

#然后执行 /sbin/sysctl -p 让参数生效

#参数说明

net.ipv4.tcp_syncookies = 1 #表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;net.ipv4.tcp_tw_reuse= 1 #表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;net.ipv4.tcp_tw_recycle= 1 #表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。net.ipv4.tcp_fin_timeout#修改系統默认的 TIMEOUT 时间

3. 基于UDP的套接字

3.1 UDP简介

1)UDP

UDP(用户数据保协议)是一个无连接的简单面向数据报的传输层协议;

UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地;

由于UDP在传输数据前不用在客户端和服务端之间建立一个连接,且没有超时重发等机制,故而传输速度很快;

UDP是一种面向无连接的协议,每一个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。

2)UDP的特点

UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口信息,由于通讯不需要连接,所以可以实现广播发送;

UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内;

UDP是一个不可靠的协议,发送方所发送的数据并不一定以相同的次序到达接收方。

3)适用情况

UDP一般多用于多点通信和实时的数据业务,它注重速度流畅。

语⾳⼴播

视频

QQ

TFTP(简单⽂件传送)

SNMP(简单⽹络管理协议)

RIP(路由信息协议,如报告股票市场,航空信息)

DNS(域名解释)

3.2 UDP的通信过程

1)图示

1278240-20200814145018565-1460217416.png

创建客户端套接字

发送/接收数据

关闭套接字

1278240-20200814150358510-41186554.png

2)UDP是无链接的,先启动哪一端都不会报错

3)udp的服务器和客户端的区分

UDP的服务端和客户端往往是通过 请求服务 和 提供服务 来进⾏区分

请求服务的⼀⽅称为:客户端

提供服务的⼀⽅称为:服务器

4)关于UDP的端口绑定

⼀般情况下,服务器端,需要绑定端⼝,⽬的是为了让其他的客户端能够正确发送到此进程

客户端⼀般不需要绑定,⽽是让操作系统随机分配,这样就不会因为需要绑定的端⼝被占⽤⽽导致程序⽆法运⾏的情况

3.3 UDP服务端&客户端

1)服务端

from socket import *ip_port= ('127.0.0.1', 9999)

buffer_size= 1024udp_server= socket(AF_INET, SOCK_DGRAM) #数据报

udp_server.bind(ip_port)#udp之所以不发生粘包的现象,是因为每次发送消息都封装一个报文头信息

#recv在自己这端的缓冲区为空时,阻塞#recvfrom在自己这端的缓冲区为空时,就收一个空?(此处描述的不准确)

#udp在发送一个空时,发送的不仅仅只是一个空,而是一个带了报文头信息的空#所以在接受时,都接受了一个带报文头信息的空

whileTrue:

data, addr= udp_server.recvfrom(buffer_size) #recvfrom接受信息的格式为 (b'消息内容', ('ip地址',端口))

print(data)

udp_server.sendto(data.upper(), addr)

2)客户端

from socket import *ip_port= ('127.0.0.1', 9999)

buffer_size= 1024udp_client=socket(AF_INET, SOCK_DGRAM)whileTrue:

msg= input('>>>').strip()

udp_client.sendto(msg.encode('utf-8'), ip_port) #sendto每次发送时都要指定ip和端口,作为第二个参数

data, addr=udp_client.recvfrom(buffer_size)print(data.decode('utf-8'))

4. 粘包现象

注意:结果的编码是以当前所在的系统为准的,如果是windows,那么读出的信息就是GBK编码的,在接收端要用GBK解码。且只能从管道里读一次结果。

3.1 粘包的产生

在发送和接收数据的过程中,发送端可以1K 、1K的发送数据,而接收端的应用程序可以2K、2K或者一次3K、6K等等的取走数据;

应用程序所看到的数据是一个整体(或者是一个流stream),一条消息有多少字节对于应用程序是不可见的,因为TCP是面向流的协议,就很容易出现粘包;

而UDP段都是一条一条消息(因为每次发送消息都封装了一个报文头信息),应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,所以UDP永远不会发生粘包。

可以认为对方一次性write/send的数据为一个消息,当对方send一条信息时,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

3.2 粘包产生的原因

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

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

3.3 详细分析粘包

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时才会清除缓冲区内容。数据是可靠的,但是会粘包。(tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的)

在tcp中send和recv其实都是在操作各自的缓冲区,并不需要客户端与服务端之间一一对应;而udp中的sendto和recvfrom要一一对应,因为没有将多个数据合并

应用程序产生的数据会拷贝一份个操作系统,然后由操作系统来发送。

3.4 发生粘包的情况

发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

3.5 拆包的发生情况

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

3.6 send(字节流)和recv(1024)及sendall

recv里指定的1024意思是从缓存里一次拿出1024个字节的数据。

send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。

3.7 低配版解决粘包问题

分析:问题的根源在于接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

缺点:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。

1)服务端

from socket import *

importsubprocess

ip_port= ('127.0.0.1', 8080)

back_log= 5buffer_size= 1024tcp_server=socket(AF_INET, SOCK_STREAM)

tcp_server.bind(ip_port)

tcp_server.listen(back_log)whileTrue:

conn, addr=tcp_server.accept()print('新的客户端连接', addr)whileTrue:try: #异常处理用于解决客户端意外断开时报异常的问题

cmd =conn.recv(buffer_size)if not cmd:break #解决客户端如果退出,服务端一直收空的问题

print('收到客户端的命令', cmd)#执行命令,得到命令的结果cmd_res

res = subprocess.Popen(cmd.decode('utf-8'), shell=True,

stderr= subprocess.PIPE, #将标准输入输出和错误全部扔到subprocess的管道中

stdout =subprocess.PIPE,

stdin=subprocess.PIPE

)

err=res.stdout.read()iferr:

cmd_res=errelse:

cmd_res=res.stdout.read()if not cmd_res: #若命令执行成功,但返回值为空(如cd .. 命令),我们就自己设置一个返回值发送给客户端以解决卡死的问题

cmd_res = "执行成功"

#解决粘包的问题---> 将要发送的消息的具体大小发送给客户端,告知其需要接收多少信息

length =len(cmd_res)

conn.send(str(length).encode('utf-8'))

client_ready= conn.recv(buffer_size) #中间插入了一个recv,造成中断,使得两次send的操作不会合并

if client_ready == b'ready': #如果接收到客户端发送过来的ready信号,服务端就开始发送真正的消息内容

conn.send(cmd_res)exceptException as e:print(e)breakconn.close()

2)客户端

from socket import *ip_port= ('127.0.0.1', 8080)

back_log= 5buffer_size= 1024tcp_client=socket(AF_INET, SOCK_STREAM)

tcp_client.connect(ip_port)whileTrue:

cmd= input('>>:').strip()if not cmd:continue

if cmd == "quit":breaktcp_client.send(cmd.encode('utf-8'))#解决粘包的问题---> 知道到底需要接收多少数据就可以解决

length = tcp_client.recv(buffer_size) #接收服务端发送过来的数据的长度

tcp_client.send(b'ready')

length= length.decode('utf-8')

recv_size= 0 #设定一个标识,以判断数据到底是否接收完毕

recv_msg = b''

while recv_size

recv_msg+= tcp_client.recv(buffer_size) #将每次接收到的数据合并

recv_size =len(recv_msg)print('命令的执行结果是', recv_msg.decode('gbk'))

tcp_client.close()

3.8 高配版解决粘包问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值