网络编程——socket开发

时间:2024-01-13 23:09:32


Socket套接字方法

socket 实例类(8-10分钟)

socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

 family(socket家族)

  • socket.AF_UNIX:用于本机进程间通讯,为了保证程序安全,两个独立的程序(进程)间是不能互相访问彼此的内存的,但为了实现进程间的通讯,可以通过创建一个本地的socket来完成
  • socket.AF_INET:(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

 socket type类型

  • socket.SOCK_STREAM #for tcp
  • socket.SOCK_DGRAM #for udp
  • socket.SOCK_RAW #原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
  • socket.SOCK_RDM #是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
  • socket.SOCK_SEQPACKET #废弃了

(Only SOCK_STREAM and SOCK_DGRAM appear to be generally useful.)

 proto=0 请忽略,特殊用途

 fileno=None 请忽略,特殊用途

服务端套接字函数(2分钟)

  • s.bind() 绑定(主机,端口号)到套接字
  • s.listen() TCP开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。 backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5, 这个值不能无限大,因为要在内核中维护连接队列
  • s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来 
  服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就返回 INVALID_SOCKET。该函数的第一个参数指定处于监听状态的流套接字;操作系统利用第二个参数来返回新创建的套接字的地址结构;操作系统利用第三个参数来返回新创建的套接字的地址结构的长度。

客户端套接字函数(2分钟)

  • s.connect() 主动初始化TCP服务器连接
  • s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数(3-5分钟)

  • s.recv() 接收数据
  • s.send() 发送数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完,可后面通过实例解释)
  • s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
  • s.recvfrom() Receive data from the socket. The return value is a pair (bytes, address)
  • s.getpeername() 连接到当前套接字的远端的地址
  • s.close() 关闭套接字
  • socket.setblocking(flag) #True or False,设置socket为非阻塞模式,以后讲io异步时会用
  • socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 返回远程主机的地址信息,例子 socket.getaddrinfo('luffycity.com',80)
  • socket.getfqdn() 拿到本机的主机名
  • socket.gethostbyname() 通过域名解析ip地址

tcp套接字

1.简单套接字
客户端和服务端:两个主要功能,1、建立链接 2、数据通讯
服务端程序会产生两个套接字socket,一个用于三次握手建立链接,另一个用于收发消息数据通讯;
客户端产生一个套接字socket,既可以用于建立链接后,再用于收发消息数据通讯。 client.py
 import socket

 #1.买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone) #2.拨号
phone.connect(('127.0.0.1',8081))
#端口范围0-65535,0-1024给操作系统用的,若一直无法连接上server,则会一直停留在这一步 #3.发收消息
phone.send('hello'.encode('utf-8'))
data = phone.recv(1024)
print(data) #4.关闭
phone.close()

service.py

 import socket

 #1.买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone) #2.绑定手机卡
phone.bind(('127.0.0.1',8081)) #端口范围0-65535,0-1024给操作系统用的 #3.开机
phone.listen(5) # 参数表示最大监听数 #4.等电话链接
print('starting...')
conn,client = phone.accept() #返回一个新的套接字conn用于通讯,client为发起通讯链接的客户端的ip和端口号
print(conn,client)
# print('===>') #5.收,发消息
data = conn.recv(1024) # 单位:bytes, 1024代表最大接收1024个bytes
print('客户端的数据',data)
conn.send(data.upper()) #6.挂电话
conn.close() #7.关机
phone.close()

2.加上循环套接字

client.py 
 import socket

 #1.买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) print(phone) #2.拨号
phone.connect(('127.0.0.1',8080)) #端口范围0-65535,0-1024给操作系统用的 while True:
msg = input('>>:').strip()
if not msg:continue
phone.send(msg.encode('utf-8')) #phone.send(b'')
print('has send') #判断能否发空
data = phone.recv(1024)
print(data.decode('utf-8')) #4.关闭
phone.close()
service.py
 import socket

 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
print(phone)
phone.bind(('127.0.0.1',8080)) #端口范围0-65535,0-1024给操作系统用的
phone.listen(5) #最大链接挂起数 print('starting...')
conn,client = phone.accept() #监听
# print('===>') #监听到到后,进行通讯循环
# while True:
# data = conn.recv(1024) # 单位:bytes, 1024代表最大接收1024个bytes
# #conn tcp协议三次握手的成果,双向链接
# if not data:break #适用与linux操作,当client单方面终止链接时,service端会出现死循环
# print('客户端的数据',data)
# conn.send(data.upper()) while True:
try:
data = conn.recv(1024) # 单位:bytes, 1024代表最大接收1024个bytes
#conn tcp协议三次握手的成果,双向链接 print('客户端的数据',data)
conn.send(data.upper())
except ConnectionResetError: break conn.close()
phone.close()

if not data:break 是用于linux的判断,因为在linux中当client端单方面终止时,servce端会一直接收到空,会一直循环print('客户端的数据',data),因此需要加上判断;
except ConnectionResetError: 是针对windows的,当client端单方面终止时,server端会报ConnnectionRsetError。
有时重启服务端时会遇到报错:

网络编程——socket开发

由于重启时系统还没来得及回收端口,因此会提示端口已被占用。

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)

解决方法:加入一条socket配置,重用ip和端口。

phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind ip和端口 前加。

或者:

实验之前要全部关闭掉所用占用端口的程序,用以下指令
linux:pkill -9 python
windows:taskkill python

3.加上 链接循环

之前代码运行可知,client端关闭后,service端也会关闭,但此刻我们想client端关闭后,service端应该能在接收新的client端的链接请求,因此,在建

立链接的部分加入循环。

client.py 
 import socket

 #1.买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) print(phone) #2.拨号
phone.connect(('127.0.0.1',8080)) #端口范围0-65535,0-1024给操作系统用的 while True:
msg = input('>>:').strip()
if not msg:continue
phone.send(msg.encode('utf-8')) #phone.send(b'')
print('has send') #判断能否发空
data = phone.recv(1024)
print(data.decode('utf-8')) #4.关闭
phone.close()
service.py

 import socket

 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
print(phone)
phone.bind(('127.0.0.1',8080)) #端口范围0-65535,0-1024给操作系统用的
phone.listen(5) #最大链接挂起数 print('starting...') while True:
'''
用于监听多次client端的链接,但一次链接发起结束后,
可继续监听下一次client端的连接
'''
conn,client = phone.accept()
print(client)
while True:
try:
data = conn.recv(1024) # 单位:bytes, 1024代表最大接收1024个bytes
#conn tcp协议三次握手的成果,双向链接
if not data: break
print('客户端的数据',data)
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()

4.模拟ssh远程执行命令

client.py 
 import socket,subprocess

 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
print(phone)
phone.bind(('127.0.0.1',9900)) #端口范围0-65535,0-1024给操作系统用的
phone.listen(5) #最大链接挂起数 print('starting...')
while True:
conn,client = phone.accept() #监听 while True: #通讯循环
try:
#1、收命令
cmd = conn.recv(1024) # 单位:bytes, 1024代表最大接收1024个bytes
#conn tcp协议三次握手的成果,双向链接
if not cmd: break
#2、执行命令、拿到结果,命令的结果存入stdout=subprocess.PIPE管道,而不是直接输出到终端
obj = subprocess.Popen(cmd.decode('utf-8'),shell=True,
# 指令由client端发送过来是以utf-8解码为bytes发送过来的,因此处应该以utf-8来编码,
# 因此此处的命令编码应该与client端的一致
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
print(obj)
stdout = obj.stdout.read()
stderr = obj.stderr.read() #s收发都是bytes格式 #3、把命令的结果返回给客户端
conn.send(stdout+stderr) #申请一块新的内存空间存放stdout+stderr,会占内存,效率会低
except ConnectionResetError:
break
conn.close() phone.close()
service.py
 import socket

 #1.买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) print(phone) #2.拨号
phone.connect(('127.0.0.1',9900)) #端口范围0-65535,0-1024给操作系统用的 while True:
msg = input('>>:').strip()
if not msg:continue
phone.send(msg.encode('utf-8'))
# 注意:信息由utf-8解码为bytes格式发送到service端,因此service端也必须把bytes格式以utf-8来编码, data = phone.recv(1024) #返回值可能超过1024bytes,
print(data.decode('gbk'))
# windows上,res.stdout.read()读出的就是GBK编码,因此此处也用GBK编码,linux上默认是utf-8 #4.关闭
phone.close()

此处注意两个小问题:

  1.service端的命令的编码应该与client端的解码模式对应,client端以utf-8解码指令为bytes,则service端必须以utf-8来编码;

  2.service端的把命令结果发送给client端,client则需要将命令结果进行编码,若serice端在windows上,则以GBK进行编码,若在linux上则以utf-8进行编码。

5.粘包现象分析

须知:只有TCP有粘包现象,UDP永远不会粘包

网络编程——socket开发

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

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

两种情况下会发生粘包:

发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据段很小,会合到一起,产生粘包)。TCP使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。

client.py
 import socket

 client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',9903)) #TCP使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,
# 合并成一个大的数据块,然后进行封包。从而在发送端造成粘包。
client.send('hello'.encode('utf-8'))
client.send('world'.encode('utf-8'))
service.py
 import socket

 service = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
service.bind(('127.0.0.1',9903))
service.listen(5) conn,addr = service.accept() res1 = conn.recv(1024)
print('第一次',res1.decode()) res2 = conn.recv(1024)
print('第二次',res2.decode())
 输出结果:
第一次 helloworld
第二次

发送端由于TCP 优化算法造成粘包

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

client.py
 #_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port) s.send('hello feng'.encode('utf-8')) 客户端
service.py
 #_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的 print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8')) conn.close() 服务端

输出结果

-----> he
-----> llo feng

接收端由于没能一次将发送端一次发送的数据全部接受,导致粘包

拆包的发生情况

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

补充问题一:为何tcp是可靠传输,udp是不可靠传输

tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的。

而udp发送数据,对端是不会返回确认信息的,因此不可靠。

补充问题二:send(字节流)和recv(1024)及sendall

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

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

send 和 recv:
1.不管是recv还是send都不是直接接收对方的数据,而是操作自己的操作系统内存-->不是一个send对应一个recv
2.recv阶段,耗时分析:
  wait data 耗时非常长
  copy data 耗时短
send耗时分析:
  copy data

5.粘包解决办法

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

第一步:先拿到数据的长度

第二步:接收真实的数据

先来介绍下struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes类型

网络编程——socket开发

但注意类型如数字是有范围的,超出范围就会报错

>>> struct.pack('i',1111111111111)

。。。。。。。。。

struct.error: 'i' format requires -2147483648 <= number <= 2147483647  #这个是范围

 import struct

 res = struct.pack('i', 1235)
print(res,type(res), len(res)) obj = struct.unpack('i', res)
print(obj)

struct 用法

输出结果

b'\xd3\x04\x00\x00' <class 'bytes'> 4
(1235,)

在数据发送端将数据长度打包,发送给接收端,解包获取实际数据的长度。

简单版本报头自制

client.py
 import socket, struct

 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone) phone.connect(('127.0.0.1',9900)) #端口范围0-65535,0-1024给操作系统用的 while True:
#1、发命令
msg = input('>>:').strip()
if not msg:continue
phone.send(msg.encode('utf-8')) #2、拿到命令的结果,并打印 #第一步:先收报头
header = phone.recv(4)
#第二步:从报头中解析出对真实数据的描述信息
total_size = struct.unpack('i',header)[0] #第三部:接收真实数据
recv_size = 0
recv_data = b''
while recv_size < total_size:
data = phone.recv(1024)
recv_data += data
recv_size += len(data) print(data.decode('gbk'))
# windows上,res.stdout.read()读出的就是GBK编码,因此此处也用GBK编码,linux上默认是utf-8 phone.close()

自制报头

service.py
 import socket,subprocess,struct

 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',9900)) #端口范围0-65535,0-1024给操作系统用的
phone.listen(5) #最大链接挂起数 print('starting...')
while True:
conn,client = phone.accept() #监听 while True: #通讯循环
try:
#1、收命令
cmd = conn.recv(1024) # 单位:bytes, 1024代表最大接收1024个bytes
#conn tcp协议三次握手的成果,双向链接
if not cmd: break
#2、执行命令、拿到结果,命令的结果存入stdout=subprocess.PIPE管道,而不是直接输出到终端
obj = subprocess.Popen(cmd.decode('utf-8'),shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
print(obj)
stdout = obj.stdout.read()
stderr = obj.stderr.read() #s收发都是bytes格式 #3、把命令的结果返回给客户端
#第一步:制作固定长度的报头
total_size = len(stdout)+len(stderr)
header = struct.pack('i', total_size) #第二步:把报头(固定长度)发送给客户端
conn.send(header) #第三步:再发送真实数据
conn.send(stdout)
conn.send(stderr) except ConnectionResetError:
break
conn.close() phone.close()

自制报头

高阶报头自制

以上讲解了简单报头的自制,但有缺陷:

1、报头存有的信息少。

2、struct模块打包的int数字有范围,普通指令的结果长度虽然不会超过这个范围,但是上传下载文件时就很有可能会超过此范围,因此下面介绍同样使用struct模块来自制跟高端的报头。

以字典的形式制作报头,字典中可以存文件名、文件md5值、文件大小等,再将字典序列化,将序列化的字符串长度通过struct pack,这样既可让报头存有足够的信息,又不会超出struct模块打包的int的数字范围。

client.py
 import socket, struct, json

 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone) phone.connect(('127.0.0.1',9900)) #端口范围0-65535,0-1024给操作系统用的 while True:
#1、发命令
msg = input('>>:').strip()
if not msg:continue
phone.send(msg.encode('utf-8')) #2、拿到命令的结果,并打印 #第一步:先收报头长度
obj = phone.recv(4)
header_size = struct.unpack('i',obj)[0]
# 第二步:再收报头
header = phone.recv(header_size) #第三步:从报头中解析出对真实数据的描述信息
header_dic = json.loads(header)
total_size = header_dic['total_size'] #第三部:接收真实数据
recv_size = 0
recv_data = b''
while recv_size < total_size:
data = phone.recv(1024)
recv_data += data
recv_size += len(data) print(data.decode('gbk'))
# windows上,res.stdout.read()读出的就是GBK编码,因此此处也用GBK编码,linux上默认是utf-8 phone.close()

高阶报头自制

service.py
 import socket, subprocess, struct, json

 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',9900)) #端口范围0-65535,0-1024给操作系统用的
phone.listen(5) #最大链接挂起数 print('starting...')
while True:
conn,client = phone.accept() #监听 while True: #通讯循环
try:
#1、收命令
cmd = conn.recv(1024) # 单位:bytes, 1024代表最大接收1024个bytes
#conn tcp协议三次握手的成果,双向链接
if not cmd: break
#2、执行命令、拿到结果,命令的结果存入stdout=subprocess.PIPE管道,而不是直接输出到终端
obj = subprocess.Popen(cmd.decode('utf-8'),shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
print(obj)
stdout = obj.stdout.read()
stderr = obj.stderr.read() #s收发都是bytes格式 #3、把命令的结果返回给客户端
#第一步:制作报头 header_dic = {
'filename':'a.txt',
'md5':'xxfdxxx',
'total_size': len(stdout)+len(stderr)
}
header_json = json.dumps(header_dic)
header_types = header_json.encode('utf-8') #第二步:把报头(固定长度)发送给客户端
conn.send(struct.pack('i',len(header_types))) #第三步:再发送报头、
conn.send(header_types) #第四步:再发送真实数据
conn.send(stdout)
conn.send(stderr) except ConnectionResetError:
break
conn.close() phone.close()

高阶报头自制


udp 协议套接字

tcp基于链接通信

  • 基于链接,则需要listen(backlog),指定连接池的大小
  • 基于链接,必须先运行的服务端,然后客户端发起链接请求
  • 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
  • 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)

udp无链接

udp 不需要经过3次握手和4次挥手,不需要提前建立连接,直接发数据就行。
  • 无链接,因而无需listen(backlog),更加没有什么连接池之说了
  • 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失
  • recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
  • 只有sendinto发送数据没有recvfrom收数据,数据丢失
  • 一个sendto对应一个recvfrom,不会产生粘包问题

udp协议虽然不会产生粘包,但 udp协议不安全,tcp协议会在发数据前发个信息,在接收端回复确认可以接收数据后,才会发送数据,发送数据          后还要等待接收端回复已接收后才会继续发送,因此tcp协议是稳定安全的。

client.py 
 import socket
# 建立套接字对象
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#client.connect(('127.0.0.0',8080)) #udp没有链接 while True:
msg = input('>>:').strip()
client.sendto(msg.encode('utf-8'),('127.0.0.1',8080)) #udp没有链接,发送信息必须指定ip和端口 data,server_addr = client.recvfrom(1024)
print(data,server_addr) client.close()

udp套接字 client

>>:hah
b'HAH' ('127.0.0.1', 8080)
>>:yy
b'YY' ('127.0.0.1', 8080)
>>:
service.py
 import socket
# 建立套接字对象
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8080)) #server.listen() #监听最大链接数,udp没有链接
#server.accept() #建立链接,udp无链接 while True:
data,client = server.recvfrom(1024)
print(data,client) #返回数据和数据发送端的ip和端口
server.sendto(data.upper(),client) #udp没有链接,发送信息必须指定ip和端口 server.close()

udp套接字 server

b'hah' ('127.0.0.1', 59001)
b'yy' ('127.0.0.1', 59001)