一、什么是socket?
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。
在设计模式中,socket其实就是一个门面模式,他把复杂的TCP/IP协议族隐藏
在socket接口后面。对用户来说,一组简单的接口就是全部,让socket
取组织数据,以符合指定的协议
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,
我们只需要遵循socket的规定取编程,写出的程序自然就是遵循
tcp/udp标准的。
也有人将socket说成是ip+port,
ip用来标识互联网中的一台主机的位置,
而port是用来标识这台机器上的一个应用程序,
ip与port的绑定就标识了互联网中独一无二的一个应用程序
##二、套接字的分类
起初,套接字被设计用在同一台主机上多个应用程序之间
多个程序之间的通讯。这也被称进程间的通讯,或IPC。
套接字有两种(或者称为有两个种族),分别是基于文件型
的和基于网络型的。
1.基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套机字调用的就是底层的文件
系统来取数据,两个套接字进程运行在同一机器,可以通过
访问同一个文件系统间接完成通信
2.基于网络类型的套接字家族
套接字家族的名字:AF_INEF
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过
他们要么是只用于某个平台,要么就是已经被废弃,或者是很少
被使用,或者是根本没有实现,所有地址家族中,AF_INET是
使用最广泛的一个,python支持很多种地址家族,但是由于我们
只关心网络编程,所以大部分时候我们只使用AF_INET)
三、基于tcp协议的套接字编程
服务端:
import socket
# 第一个参数:基于网络的套接字 第二个参数:流的方式
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8000))
phone.listen(5)#允许连接数
print('===============')
conn,addr = phone.accept()# (本质是在进行三次握手)
msg = conn.recv(1024)#接收信息的字节大小
print('客户端发来的消息是',msg)
#发消息
conn.send(msg)
conn.close()#
phone.close()
客户端:
import socket
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)
上面就是一个简单的基于tcp协议的套接字编程,服务端只能接收一个客户端的连接,服务端和客户端通信完成后,直接断开连接,服务端和客户端都关闭
2.客户端和服务端循环收发消息:
服务端:
# import socket
from socket import *
ip_port = ('127.0.0.1',8081)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
print('服务端开始运行了')
conn,addr = tcp_server.accept()#
print('双向连接是',conn)
print('客户端地址',addr) #ip地址加这个进程的端口号
while True:
data = conn.recv(buffer_size)
print('客户端发来的消息是',data.decode('utf-8'))
conn.send(data.upper())
conn.close()
tcp_server.close()
客户端:
# import socket
from socket import *
ip_port = ('127.0.0.1',8081)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
msg = input('>>:').strip()
tcp_client.send(msg.encode('utf-8'))
print('客户端已经发送消息')
data = tcp_client.recv(buffer_size)
print('收到服务端发来的消息',data.decode('utf-8'))
tcp_client.close()
注意:这里的服务端虽然可以和客户端循环发送消息,但还是只能连接一个客户端,且两端都不会断掉。
3.socket收发消息原理剖析:
服务端:
# import socket
from socket import *
ip_port = ('127.0.0.1',8084)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
print('服务端开始运行了')
conn,addr = tcp_server.accept()#
print('双向连接是',conn)
print('客户端地址',addr)
while True:
data = conn.recv(buffer_size)
print('客户端发来的消息是',data.decode('utf-8'))
# 服务端 当发送的消息,即msg为空的时候,recv这里会卡住(卡在了自己的内核态内存)
#socket服务端和客户端的程序都是在应用层的,而且他们都是存在各自的用户态内存的,
# 而他们发送send和接收recv的消息(想发送消息必须要操作网卡,而只有操作系统才能操作网卡)
# 都是在各自的内核态内存的(socket发送消息,先把数据包发
# 到自己的内核态内存,由操作系统调用网卡和数据包发送给对方,接收消息的时候也是在
# 自己的内核态内存中取数据包)。
#当客户端发送的消息为空时,先把这个空发送到自己的
#内核态内存,然后操作系统检测到你发送的消息是空,
#所以就不会把消息发送给服务端,故而就造成了卡住了
#
conn.send(data.upper())
conn.close()
tcp_server.close()
客户端:
# import socket
from socket import *
ip_port = ('127.0.0.1',8084)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
msg = input('>>:').strip()
#因为输入空 会在recv卡住,所以要进行空判断
# if not msg:
# continue
tcp_client.send(msg.encode('utf-8'))
print('客户端已经发送消息')
# 客户端 当发送的消息,即msg为空的时候,recv这里会卡住(卡在了自己的内核态内存)
data = tcp_client.recv(buffer_size)
print('收到服务端发来的消息',data.decode('utf-8'))
tcp_client.close()
4.基于UDP的套接字
服务端:
from socket import *
ip_port = ('127.0.0.1',8082)
buffer_size = 1024
udp_server = socket(AF_INET,SOCK_DGRAM)#第二个参数 代表数据报式的套接字
udp_server.bind(ip_port)
while True:
data,addr = udp_server.recvfrom(buffer_size)
print(data.decode('utf-8'))
udp_server.sendto(data.upper(),addr)
客户端:
'''
recv在自己这段的缓冲区为空时,阻塞
recvfrom在自己这段的缓冲区为空时,就收一个空
tcp是基于数据流的,于是收发消息不能为空,这就需要在
客户端和服务端都添加空消息的处理机制,防止程序卡住,
而udp是基于数据报的,即使是你输入的是空内容(直接
回车),那也不是空消息,udp协议会帮你封装上消息头
'''
from socket import *
ip_port = ('127.0.0.1',8082)
buffer_size = 1024
udp_client = socket(AF_INET,SOCK_DGRAM)#第二个参数 代表数据报式的套接字
while True:
msg = input('>>').strip()
udp_client.sendto(msg.encode('utf-8'),ip_port)
data,addr = udp_client.recvfrom(buffer_size)
print(data.decode('utf-8'))
5.基于tcp的粘包问题:
服务端:
from socket import *
ip_port = ('127.0.0.1',8084)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
conn,addr = tcp_server.accept()
data1 = conn.recv(5)#指定一次接收的字节,就解决了粘包的问题
#tcp是面向连接的,有优化算法,一次接收不完,会接着继续接收
#但是udp不是面向连接的,没有优化算法,
print('第一次数据',data1)
data2 = conn.recv(5)
print('第二次数据',data2)
data3 = conn.recv(5)
print('第三次数据',data3)
客户端:
'''
数据量小的粘包
'''
from socket import *
ip_port = ('127.0.0.1',8084)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
#套接字不是一个发对应一个收(服务端和客户端有自己的缓冲区)
tcp_client.send('helloworldchuan'.encode('utf-8'))
6.解决粘包的方法:
'''
#
'''
'''
粘包解决方案
由于应用程序自己发送的数据可以进行打包处理,自己制作协议,对数据进行封装添
加报头,然后发送数据部分。而报头必须是固定长度,对方接受时可以先接受报头,对
报头进行解析,然后根据报头内的封装的数据的长度对数据进行读取,这样收取的数据
就是一个完整的数据包
'''
#解决粘包版本
from socket import *
import subprocess
import struct
ip_port = ('127.0.0.1',8086)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
while True:
conn,addr = tcp_server.accept()
print('新的客户端链接',addr)
while True:
# 客户端的conn.recv和服务端的tcp_client.send是三次握手的过程
cmd = conn.recv(buffer_size) #
if not cmd:
break
print('收到客户端的命令', cmd)
res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE)
err = res.stderr.read()
if err:
cmd_res = err
else:
cmd_res = res.stdout.read()
# 发
length = len(cmd_res)
data_length = struct.pack('i',length)
conn.send(data_length)
conn.send(cmd_res)
conn.close()
'''
'''
'''
粘包解决方案
由于应用程序自己发送的数据可以进行打包处理,自己制作协议,对数据进
行封装添加报头,然后发送数据部分。而报头必须是固定长度,对方接受时
可以先接受报头,对报头进行解析,然后根据报头内的封装的数据的长度对
数据进行读取,这样收取的数据就是一个完整的数据包
'''
from socket import *
import struct
ip_port = ('127.0.0.1',8086)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
cmd = input('>>:').strip()
if not cmd:continue
if cmd == 'quit':break
tcp_client.send(cmd.encode('utf-8'))
#解决粘包的位置
#接收报头
length_data = tcp_client.recv(4)
#对报头解压
length = struct.unpack('i',length_data)[0]
recv_size = 0
recv_msg = b''
while recv_size < length:
#写法一
# r_m = tcp_client.recv(buffer_size)
# recv_msg+=r_m
# recv_size+=len(r_m)
#写法二
recv_msg += tcp_client.recv(buffer_size)
recv_size = len(recv_msg)
print('命令的执行结果是',recv_msg.decode('gbk'))
tcp_client.close()