Python Socket 编程

Python Socket 编程

Python 提供了两个基本的 socket 模块:

  • Socket 它提供了标准的BSD Socket API。
  • SocketServer 它提供了服务器重心,可以简化网络服务器的开发。

下面讲解下 Socket模块功能。

Socket 类型

套接字格式:socket(family, type[,protocal]) 使用给定的套接族,套接字类型,协议编号(默认为0)来创建套接字

socket 类型 描述
family
socket.AF_UNIX UNIX系统进程间传输数据
socket.AF_INET IPv4网络传输数据(IPv4
socket.AF_INET6 IPv6网络传输数据
Type
TCP socket.SOCK_STREAM 基于TCP的流式socket通信,面向连接可靠的传输,TCP传输
UDP socket.SOCK_DGRAM 基于UDP的数据报式socket通信,面向无连接不可靠的传输,UDP传输
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次SOCK_RAW也可以处理特殊的IPV4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头
socket.SOCK_SEQPACKET 连续的数据包传输(已废弃)

####套接字家族

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

unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信。

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

  还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET

创建TCP Socket

1
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

创建UDP Socket

1
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

Socket 函数

  • TCP发送数据时,已建立好TCP链接,所以不需要指定地址,而UDP是面向无连接的,每次发送都需要指定发送给谁。

  • 服务器与客户端不能直接发送列表,元素,字典等带有数据类型的格式,发送的内容必须是字符串数据。socket传输字符串需要bytes

    总结

    1、socket分为服务端和客户端。

    2、TCP传输不需要IP,UDP传输需要IP地址。

    3、socket传输字符串需要变成byte型。

    4、列表、字典等数据也需要成变byte型。json处理过的数据是字符型的,decode后可以进行send。

    5、传输大数据,使用长度时,要注意len的对象是原数据,还是encode后的数据,接收方也得计算相应的数据。否则会造成文件长度不匹配

服务器端 Socket 函数

Socket 函数 描述
s.bind(address) 将套接字绑定到地址,在AF_INET下,以tuple(host, port)的方式传入,如s.bind((host, port))
s.listen(backlog) 开始监听TCP传入连接,backlog指定在拒绝链接前,操作系统可以挂起的最大连接数,该值最少为1,大部分应用程序设为5就够用了(客户端连接数量,数字)
s.accept() 接受TCP链接并返回(conn, address),其中conn是新的套接字对象(即,接受的信息),可以用来接收和发送数据,address是链接客户端的地址(ip,随机端口)。

客户端 Socket 函数

Socket 函数 描述
s.connect(address) 链接到address处的套接字,一般address的格式为tuple(host, port),如果链接出错,则返回socket.error错误(绑定服务端地址)
s.connect_ex(address) 功能与s.connect(address)相同,但成功返回0,失败返回errno的值

公共 Socket 函数

Socket 函数 描述
s.recv(bufsize[, flag]) 接受TCP套接字的数据,数据以字符串形式返回,buffsize指定要接受的最大数据量,flag提供有关消息的其他信息,通常可以忽略。
bufsize官方建议8192,不同系统最大数值不同,一般一次可以收10M左右。
s.send(string[, flag]) 发送TCP数据,将字符串中的数据发送到链接的套接字,返回值是要发送的字节数量,该数量可能小于string的字节大小
s.sendall(string[, flag]) 完整发送TCP数据,将字符串中的数据发送到链接的套接字,但在返回之前尝试发送所有数据。成功返回None,失败则抛出异常
s.recvfrom(bufsize[, flag]) 接受UDP套接字的数据u,与recv()类似,但返回值是tuple(data, address)。其中data是包含接受数据的字符串,address是发送数据的套接字地址
s.sendto(string[, flag], address) 发送UDP数据,将数据发送到套接字,address形式为tuple(ipaddr, port),指定远程地址发送,返回值是发送的字节数
s.close() 关闭套接字
s.getpeername() 返回套接字的远程地址,返回值通常是一个tuple(ipaddr, port)
s.getsockname() 返回套接字自己的地址,返回值通常是一个tuple(ipaddr, port)
s.setsockopt(level, optname, value) 设置给定套接字选项的值
s.getsockopt(level, optname[, buflen]) 返回套接字选项的值
s.settimeout(timeout) 设置套接字操作的超时时间,timeout是一个浮点数,单位是秒,值为None则表示永远不会超时。一般超时期应在刚创建套接字时设置,因为他们可能用于连接的操作,如s.connect()
s.gettimeout() 返回当前超时值,单位是秒,如果没有设置超时则返回None
s.fileno() 返回套接字的文件描述
s.setblocking(flag) 如果flag为0,则将套接字设置为非阻塞模式,否则将套接字设置为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile() 创建一个与该套接字相关的文件
1
2

port = socket.getservbyname('ssh','tcp') # /etc/services端口号

Socket 编程思想

1
2
3
4
5
6
7
8
9
10
11
12
13

import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIXAF_INET。socket_type 可以是 SOCK_STREAMSOCK_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)

TCP 服务器
1、创建套接字,绑定套接字到本地IP与端口

1
2
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind()

2、开始监听链接

1
s.listen()

3、进入循环,不断接受客户端的链接请求

1
2
While True:
s.accept()

4、接收客户端传来的数据,并且发送给对方发送数据

1
2
s.recv()
s.sendall()

5、传输完毕后,关闭套接字

1
s.close()

TCP 客户端
1、创建套接字并链接至远端地址

1
2
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect()

2、链接后发送数据和接收数据

1
2
s.sendall()
s.recv()

3、传输完毕后,关闭套接字

#####send和sendall区别

1
data = s.recv(1024)

1024 是缓冲区数据大小限制最大值参数 bufsize,并不是说 recv() 方法只返回 1024个字节的内容

send() 方法也是这个原理,它返回发送内容的字节数,结果可能小于传入的发送内容,你得处理这处情况,按需多次调用 send() 方法来发送完整的数据

应用程序负责检查是否已发送所有数据;如果仅传输了一些数据,则应用程序需要尝试传 递剩余数据 引用

我们可以使用 sendall() 方法来回避这个过程

和 send() 方法不一样的是,sendall() 方法会一直发送字节,只到所有的数据传输完成 或者中途出现错误。成功的话会返回 None 引用

via

via

socket(创建套接字) —> bind(绑定地址) —> listen(设置监听)—> accept(等待链接) —> recv/send(收/发消息) —> close ()

收发函数特性:

recv特征:

  1. 如果建立的另一端链接被断开, 则recv立即返回空字符串
  2. recv接受缓冲区取出内容,当缓冲区为空则阻塞
  3. recv如果一次接受不完缓冲区的内容下次执行会自动接受

send特征:

  1. ​ 如果发送的另一端不存在则会产生Pipe Broken异常

  2. send发送缓冲区发送内容,缓冲为满则堵塞

Socket tcp服务器端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

import socket


HOST = '192.168.15.46'
PORT = 8002

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

s.bind((HOST,PORT)) #
s.listen() #可运行的客户端


print('server start %s:%s'%(HOST,PORT))

print('wait...')


while 1: #
conn,addr = s.accept()# 接受一套接信息,和地址。
print('conning',addr)
while 1:
data = conn.recv(1024) #接受客户端数据,可以改变接受的数据,但不小于客户端发送的消息
data = data.decode('utf-8')# 解码,在传输中是bytes传输。
print(data) #打印包数据
msg = input('plese msg:')
msg = msg.encode('utf-8')
#conn.send(b'hello') # 在网络传输中,变成字节数据,才可以传输

conn.send(msg)

s.close()



Socket tcp客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import socket
HOST = '192.168.15.46'
PORT = 8002

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #创建一个套接字对象

s.connect((HOST,PORT))

print('conneting')
while 1:
msg = input('please input msg:')
s.send(msg.encode('utf-8'))
data = s.recv(1024) #接受返回的数据包,通信的主体,一收一发
data = data.decode('utf-8')
print(data)
s.close()

基于udp的服务端编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
基于udp的服务端编程:
1.创建套接字:
sockfd = socket(AF_INET,SOCK_DGRAM)
2.绑定地址:
sockfd.bind()
3.消息收发
data, addr = sockfd.recvfrom(buffersize)
功能:接受udp消息
参数:接受消息的大小
返回值:
data 接受到的内容
addr 消息发送的地址

recvfrom每次接受一个报文,如果没有接受到的内容则直接丢弃
sockfd.sendto(data, addr)
功能:udp消息发送
参数:
data 要发送的内容 bytes
addr 目标地址
返回:发送字节数
4.关闭套接字:
socket.close()

次1

次2

问题:

有的同学在重启服务端时可能会遇到

2018-10-17 at 5.34 PM

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

解决方法:

1
2
3
4
5
#加入一条socket配置,重用ip和端口

server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1',8080))

粘包现象

让我们基于tcp先制作一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig)

1
2
3
4
5
6
7
8
9
10
11
12
13

import subprocess
cmd = input('>>')
subinfo = subprocess.Popen(
cmd, #字符串指令:'dir','ipconfig',等等
shell=True, #使用shell,就相当于使用cmd窗口
stderr=subprocess.PIPE, #标准错误输出,凡是输入错误指令,错误指令输出的报错信息就会被它拿到
stdout=subprocess.PIPE #标准输出,正确指令的输出结果被它拿到
)

print(subinfo.stdout.read().decode('utf-8')) # locale 查看

print(subinfo.stderr.read().decode('utf-8')) # win需要解码为gbk

   注意:

   如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码

  且只能从管道里读一次结果,PIPE称为管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from socket import *
import subprocess

ip_port=('127.0.0.1',8080)
BUFSIZE=1024

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)

while True:
conn,addr=tcp_socket_server.accept()
print('客户端',addr)

while True:
cmd=conn.recv(BUFSIZE)
if len(cmd) == 0:break

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

stderr=act_res.stderr.read()
stdout=act_res.stdout.read()
conn.send(stderr)
conn.send(stdout)

什么是粘包

须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来

首先需要掌握一个socket收发消息的原理

两种情况会发生粘包

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#server
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(10)
data2=conn.recv(10)

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()

Client

1
2
3
4
5
6
7
8
9
10
11
12

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'.encode('utf-8'))
s.send('feng'.encode('utf-8'))

拆包的发生情况

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

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

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

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

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

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

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

解决粘包的low比处理方法

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

low版本的解决方法

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import socket
import subprocess

ip_port = ('127.0.0.1', 8889)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(ip_port)
s.listen()

while True:
conn, addr = s.accept()
print('客户端', addr)
while True:
msg = conn.recv(1024)
if not msg:
break
#
res = subprocess.Popen(
msg.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
err = res.stderr.read()
if err:
ret = err
else:
ret = res.stdout.read()
data_length = len(ret)
conn.send(str(data_length).encode('utf-8'))
data = conn.recv(102).decode('utf-8')
if data == 'recv_ready':#???
conn.sendall(ret)
conn.close()

client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import socket

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


# res = s.connect_ex(('127.0.0.1',8889))
res = s.connect(('127.0.0.1',8889))

while 1:
msg = input('>>>')
if len(msg) == 0:continue#没输入,则重新输入
if msg == 'quit':break#退出
s.send(msg.encode('utf-8'))#发送执行的命令
length = int(s.recv(1024).decode('utf-8'))#接受数据包长度(数字转整数性下面做计算
print(length)
s.send('recv_ready'.encode('utf-8'))#发送一个可以传输的标示。
send_size = 0
recv_size = 0
data = b''
while recv_size < length:#校验数据完整性
data += s.recv(102)#循环缓存池能接受收的数据
print(len(data))
recv_size += len(data)#接收数据包长度对比发送方的长度,才算完整数据。
print(data.decode('utf-8'))#输出接受到完整信息
print("计算后接受包大小",recv_size)
print(len(data))


#接收方、发送方两端

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!