基于socket的网络编程
网络通信其实就是Socket间的通信。
“两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。”
可以这么说,Socket就是一个网络编程的接口(API),它定义了一种标准,并对TCP/IP进行封装,实现了网络传输数据的能力。
1. socket 小结
1.1 套接字的三个属性
套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。
- 套接字的域:AF_INET、AF_UNIX
前者指的是Internet网络,后者表示UNIX文件系统 - 套接字的端口号:范围是0-65535
- 套接字协议类型:SOCK_STREAM、SOCK_DGRAM、SOCKET_RAW(原始套接字)
SOCK_STREAM
(流套接字)在域中通过TCP/IP连接
实现,同时也是AF_UNIX中常用的套接字类型;
SOCK_DGRAM
(数据报套接字)在域中通常是通过UDP/IP协议
实现;
SOCKET_RAW
(原始套接字)允许对较低层次的协议直接访问,比如IP、 ICMP协议
1.2 socket 通信基本流程图
1.3 TCP
TCP 网络编程
TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。
首先,客户端和服务端会分别新建一个socket,服务端的socket需要通过bind()来绑定上端口,启动listen()进行实时监听,并等待客户端的接入,即accept()。而客户端则需要通过服务器IP和端口两个参数来建立connect()连接,此时,服务器会得到有新客户端连接的信息,启动read()等待客户端数据的传人,客户端如果成功接收到服务端的连接成功后,继续执行write()来向服务端发生数据,同理,服务端也使用这样的模式回馈客户端的数据,知道客户端关闭,服务端会收到客户端退出连接的消息,服务器重新进入等待状态,等待新客户端的进入。
Python代码:
-
服务端
import socket #服务端 new_socket = socket.socket() # 创建 socket 对象 ip = "127.0.0.1" # 获取本地主机名 port = 52052 # 设置端口 new_socket.bind((ip, port)) # 绑定端口 new_socket.listen(5) # 等待客户端连接并设置最大连接数 while True: new_cil, addr = new_socket.accept() # 建立客户端连接。 print('新进来的客户端的地址:', addr) print(new_cil.recv().decode()) new_cil.send('答案为6') new_cil.close() # 关闭连接
-
客户端
import socket #客户端 ip = "127.0.0.1" port = 52052 new_socket = socket.socket() #创建socket对象 new_socket.connect((ip,port)) #连接 new_socket.send("请求给我计算下1+5=多少?".encode(encoding='utf-8')) #发生数据 print("客户端发给服务端:请求给我计算下1+5=多少?") back_str = new_socket.recv().decode() #结束数据 print("服务端发给客户端:"+back_str) new_socket.close() #关闭客户端 print("客户端结束运行")
小坑:encode() 和 decode()
关于 encode() 和 decode():
- encode()编码 :
str -> bytes
,str.encode() - decode()解码 :
bytes -> str
,bytes.decode()
而在 socket 编程中:
- socket.send(
bytes
) - socket.recv(
bytes
),注意:其结果是str
格式!! - print(
str
)
因此注意数据格式的转换!!!
1.4 UDP(暂略)
2. Python socket 类
这个Python接口是用Python的面向对象风格对Unix系统调用和套接字库接口的直译:函数 socket() 返回一个 套接字对象 ,其方法是对各种套接字系统调用的实现。
参见:
- 模块:socketserver
用于简化网络服务端编写的类。 - 模块:ssl
套接字对象的TLS/SSL封装。
socket 实例类:socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
注:
proto=0 请忽略,特殊用途
fileno=None 请忽略,特殊用途
family(socket家族)
-
socket.AF_UNIX:用于本机进程间通讯,为了保证程序安全,两个独立的程序(进程)间是不能互相访问彼此的内存的,但为了实现进程间的通讯,可以通过创建一个本地的socket来完成
-
socket.AF_INET:表示ipv4,还有AF_INET6被用于ipv6。
-
所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET
socket type类型
socket.SOCK_STREAM # for tcp
socket.SOCK_DGRAM # for udp
服务端套接字函数
s.bind()
绑定(主机,端口号)到套接字s.listen()
开始TCP监听s.accept()
被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect()
主动初始化TCP服务器连接s.connect_ex()
connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
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地址
3. 小结
总结
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。
附:粘包
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
接收方不知道接受消息的界限(可能没有接受完,留在管道里,下次会继续接受,和后面的黏在一起了)。另一方面是,底层优化算法,将时间间隔短,小的数据包合成一个包发送。接收端不知道是哪一次的,小的数据就会一次接受出现粘包。
3. 应用
3.1 基于socket开发一个聊天程序,实现两端互相发送和接收消息
使用:先打开 server 端,再再另一个窗口打开 client 端。
-
client
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(('127.0.0.1', 8083)) while True: msg = input('>>>: ').strip() # msg='' if not msg:continue phone.send(msg.encode('utf-8')) # phone.send(b'') data = phone.recv(1024) print(data.decode('utf-8')) phone.close()
-
server
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) phone.bind(('127.0.0.1', 8083)) #0-65535:0-1024给操作系统使用 phone.listen(5) print('starting...') while True: # 连接循环 conn, client_addr = phone.accept() print(client_addr) while True: # 通讯循环 try: data = conn.recv(1024).decode('utf-8') # decode('utf-8')支持显示中文 if not data:break #适用于linux操作系统 print('From client:', data) response = input('>>>:').strip() conn.send(response.encode()) except ConnectionResetError: #适用于windows操作系统 break conn.close() phone.close()
效果:
-
server
-
client
3.2 基于tcp socket,开发简单的远程命令执行程序,允许用户执行命令,并返回结果
- server
import socket
import subprocess
import struct
import json
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True: # 链接循环,保证客户端断开时,服务端不断开
conn,client_addr=phone.accept()
print(client_addr)
while True: #通信循环
try:
#1、收命令
cmd=conn.recv(8096)
if not cmd:break #适用于linux操作系统
#2、执行命令,拿到结果
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
#3、把命令的结果返回给客户端
#第一步:制作固定长度的报头(包含数据的长度)
header_dic={
'filename':'%s.txt'%cmd,
'md5':'xxdxxx',
'total_size': len(stdout) + len(stderr)
}
header_json=json.dumps(header_dic)
header_bytes=header_json.encode('utf-8')
#第二步:利用struct后,将处理后的报头长度发送(这样报头长度固定的)
conn.send(struct.pack('i',len(header_bytes)))
#json处理后,不用担心数据长度过长,导致使用struct出错
#第三步:再发报头(包含数据的长度)
conn.send(header_bytes)
#第四步:再发送真实的数据(直接发数据)
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError: #适用于windows操作系统
break
conn.close()
phone.close()
- client
import socket
import struct
import json
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',9909))
while True:
#1、发命令
cmd=input('>>: ').strip() #ls /etc
if not cmd:continue
phone.send(cmd.encode('utf-8'))
#2、拿命令的结果,并打印
#第一步:先收报头的长度
obj=phone.recv(4)
header_size=struct.unpack('i',obj)[0]
#第二步:再收报头(此处的报头是struct处理过的)
header_bytes=phone.recv(header_size)
#第三步:从报头中解析出对真实数据的描述信息
header_json=header_bytes.decode('utf-8')
header_dic=json.loads(header_json)
print(header_dic)
total_size=header_dic['total_size']
#第四步:接收真实的数据
recv_size=0
recv_data=b''
while recv_size < total_size:
res=phone.recv(1024)
recv_data+=res
recv_size+=len(res)
print(recv_data.decode('utf-8'))
phone.close()
3.3 基于tcp协议编写简单FTP程序,实现上传、下载文件功能,并解决粘包问题
- server
import socket
import subprocess
import struct
import json
import os
BASE_DIR = os.path.dirname(__file__)
db_dir=os.path.join(BASE_DIR,'share')
def get(conn,cmds):
filename = cmds[1]
# 3、以读的方式打开文件,读取文件内容发送给客户端
# 第一步:制作固定长度的报头
header_dic = {
'filename': filename, # 'filename':'1.mp4'
'md5': 'xxdxxx',
'file_size': os.path.getsize(r'%s/%s' % (db_dir, filename))
# os.path.getsize(r'/Users/linhaifeng/PycharmProjects/网络编程/05_文件传输/server/share/1.mp4')
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')
# 第二步:先发送报头的长度
conn.send(struct.pack('i', len(header_bytes)))
# 第三步:再发报头
conn.send(header_bytes)
# 第四步:再发送真实的数据
with open('%s/%s' % (db_dir, filename), 'rb') as f:
# conn.send(f.read())
for line in f:
conn.send(line)
def put(phone,cmds):
# 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
# 第一步:先收报头的长度
obj = phone.recv(4)
header_size = struct.unpack('i', obj)[0]
# 第二步:再收报头
header_bytes = phone.recv(header_size)
# 第三步:从报头中解析出对真实数据的描述信息
header_json = header_bytes.decode('utf-8')
header_dic = json.loads(header_json)
'''
header_dic={
'filename': filename, #'filename':'1.mp4'
'md5':'xxdxxx',
'file_size': os.path.getsize(filename)
}
'''
print(header_dic)
total_size = header_dic['file_size']
filename = header_dic['filename']
# 第四步:接收真实的数据
with open('%s/%s' % (db_dir, filename), 'wb') as f:
recv_size = 0
while recv_size < total_size:
line = phone.recv(1024) # 1024是一个坑
f.write(line)
recv_size += len(line)
print('总大小:%s 已下载大小:%s' % (total_size, recv_size))
def run():
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8912)) #0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True: # 链接循环
conn,client_addr=phone.accept()
print(client_addr)
while True: #通信循环
try:
#1、收命令
res=conn.recv(8096) # b'put 1.mp4'
if not res:break #适用于linux操作系统
#2、解析命令,提取相应命令参数
cmds=res.decode('utf-8').split() #['put','1.mp4']
if cmds[0] == 'get':
get(conn,cmds)
elif cmds[0] == 'put':
put(conn,cmds)
except ConnectionResetError: #适用于windows操作系统
break
conn.close()
phone.close()
if __name__ == '__main__':
run()
- client
import socket
import struct
import json
import os
BASE_DIR = os.path.dirname(__file__)
db_dir=os.path.join(BASE_DIR,'download')
def get(phone,cmds):
# 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
# 第一步:先收报头的长度
obj = phone.recv(4)
header_size = struct.unpack('i', obj)[0]
# 第二步:再收报头
header_bytes = phone.recv(header_size)
# 第三步:从报头中解析出对真实数据的描述信息
header_json = header_bytes.decode('utf-8')
header_dic = json.loads(header_json)
'''
header_dic={
'filename': filename, #'filename':'1.mp4'
'md5':'xxdxxx',
'file_size': os.path.getsize(filename)
}
'''
print(header_dic)
total_size = header_dic['file_size']
filename = header_dic['filename']
# 第四步:接收真实的数据
with open('%s/%s' % (db_dir, filename), 'wb') as f:
recv_size = 0
while recv_size < total_size:
line = phone.recv(1024) # 1024是一个坑
f.write(line)
recv_size += len(line)
print('总大小:%s 已下载大小:%s' % (total_size, recv_size))
def put(conn,cmds):
filename = cmds[1]
# 3、以读的方式打开文件,读取文件内容发送给客户端
# 第一步:制作固定长度的报头
header_dic = {
'filename': filename, # 'filename':'1.mp4'
'md5': 'xxdxxx',
'file_size': os.path.getsize(r'%s/%s' % (db_dir, filename))
# os.path.getsize(r'/Users/linhaifeng/PycharmProjects/网络编程/05_文件传输/server/share/1.mp4')
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')
# 第二步:先发送报头的长度
conn.send(struct.pack('i', len(header_bytes)))
# 第三步:再发报头
conn.send(header_bytes)
# 第四步:再发送真实的数据
with open('%s/%s' % (db_dir, filename), 'rb') as f:
# conn.send(f.read())
for line in f:
conn.send(line)
def run():
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8912))
while True:
#1、发命令
inp=input('>>: ').strip() #get a.txt
if not inp:continue
phone.send(inp.encode('utf-8'))
cmds=inp.split() #['get','a.txt']
if cmds[0] == 'get':
get(phone,cmds)
elif cmds[0] == 'put':
put(phone,cmds)
phone.close()
if __name__ == '__main__':
run()
参考: