一、Socket原理
学习了大佬的知识,简单记一些笔记
https://www.jianshu.com/p/066d99da7cbd
http://c.biancheng.net/view/2351.html
1.1什么是Socket
在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。
我的理解就是Socket就是该模式的一个实现:即socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
百度百科:套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合
1.2Unix/Lunix中的socket
在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
你也许听很多高手说过,UNIX/Linux 中的一切都是文件!那个家伙说的没错。
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
请注意,网络连接也是一个文件,它也有文件描述符!你必须理解这句话。
我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
用 read() 读取从远程计算机传来的数据;
用 write() 向远程计算机写入数据。
你看,只要用 socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!
1.3Windows中的socket
Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。因此,本教程如果涉及 Windows 平台将使用“句柄”,如果涉及 Linux 平台则使用“描述符”。
与 UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专门针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了
2.网络中进程如何通信
2.1、本地进程间通信
a、消息传递(管道、消息队列、FIFO)
b、同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
c、共享内存(匿名的和具名的,eg:channel)
d、远程过程调用(RPC)
2.2、 网络中进程如何通信
我们要理解网络中进程如何通信,得解决两个问题:
a、我们要如何标识一台主机,即怎样确定我们将要通信的进程是在那一台主机上运行。
b、我们要如何标识唯一进程,本地通过pid标识,网络中应该怎样标识?
解决办法:
a、TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机
b、传输层的“协议+端口”可以唯一标识主机中的应用程序(进程),因此,我们利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互
3、Socket通信分类
网络中进程间利用三元组【ip地址,协议,端口】可以进行网络间通信,socket就是利用三元组解决网络通信的一个中间件工具,就目前而言,几乎所有的应用程序都是采用Socket,Socket通信的数据传输方式,常用的有两种:
a、SOCK_STREAM:对应TCP协议,表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。
b、SOCK_DGRAM:对应UDP协议,表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
例如:QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响
Socket 编程是基于 TCP 和 UDP 协议的,它们的层级关系如下图所示:
图解Socket() 函数
4、Socket(TCP)建立连接的三次握手
Socket的功能简化为三个:建立连接、发送数据以及接收数据,下链接为建立连接的流程
http://c.biancheng.net/view/2351.html
Socket缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发
送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取
这些I/O缓冲区特性可整理如下:
- I/O缓冲区在每个TCP套接字中单独存在;
- I/O缓冲区在创建套接字时自动生成;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
一般情况下,不用关系默认缓冲区的大小,但也可以用如下代码查看与修改
sock.getsockopt() # 获取缓冲区大小
sock.setsockopt() #更改缓冲区大小
收发函数特性:
recv特征:
-
如果建立的另一端链接被断开, 则recv立即返回空字符串
-
recv是从接受缓冲区取出内容,当缓冲区为空则阻塞
-
recv如果一次接受不完缓冲区的内容,下次执行会自动接受
send特征:
-
如果发送的另一端不存在则会产生Pipe Broken异常
-
send是从发送缓冲区发送内容,当缓冲区为满则堵塞
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性,TCP套接字默认情况下是阻塞模式,也是最常用的。
TCP协议的粘包问题
socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。
例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。
假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。
这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
真正的关闭socket
close方法可以释放一个连接的资源,但是不是立即释放,如果想立即释放,那么请在close之前使用shutdown方法
shutdown方法是用来实现通信模式的,模式分三种,SHUT_RD 关闭接收消息通道,SHUT_WR 关闭发送消息通道,SHUT_RDWR 两个通道都关闭
也就是说,想要关闭一个连接,首先把通道全部关闭,然后在release连接,以上三个静态变量分别对应数字常量:0,1,2
self.tcpClient.shutdown(2) #关闭消息发送通道
self.tcpClient.close() #关闭套接字连接
python socket编程实例1——文本传输
客户端:
#port = str(input('please input sever port:'))
host = '192.168.2.107' #客户端连接到服务器的ip
port = 5270 #端口
sever_address = (host, port) #元组定义服务器地址,用于作为socket.connect()函数参数 连接到服务器
text_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #创建一个socket对象为text_client
text_client.connect(sever_address) #连接到服务器
succeed_flag='cok' #设置消息发送成功标志
text = 'connect succeed'
while True :
try:
text_client.send(text.encode()) # 发送文本数据,用 encode() 方法将str变为byte
text = input('please input the message')
receive_text = text_client.recv(1024).decode()
print(receive_text)
finally:
print('send over!')
服务器:
import socket
#可以手动输入本机ip地址,若有多个网口,服务器想从那个网口接收数据,就输入那个网口的ip
#hostname = socket.gethostname() #可以用 .gethostname()函数来自动得到主机ip,不用手动输入了
#host = socket.gethostbyname(hostname)
host = '192.168.2.107' #客户端连接到服务器的ip
port = 5270 #端口
sever_address = (host, port) #创建元组作为 socket.bind()函数的输入,
text_sever = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #创建一个socket对象为text_sever 为服务器
text_sever.bind(sever_address) #.bind() 函数绑定端口,该服务器监听此端口
text_sever.listen(4) #开启监听,同时接入数量最多为4
succeed_flag = 'sok'
while True :
try:
print(host)
print('waiting connect')
text_client_socket, text_client_address = text_sever.accept() #accept() 函数,堵塞等待client的连接,连接到后才会执行下一条语句
print(text_client_address[0] + 'is connected!')
while True :
receive_text = text_client_socket.recv(1024) .decode() #接收client发送的数据,数据最大为1024 ;此处可以看出接收用户数据测试
print(receive_text)
text_client_socket.send(succeed_flag.encode()) #发送给client ok ,反馈自己确实接收到数据
finally:
print('work over!')
python socket编程实例2——视频传输
客户端:
#-*- coding: UTF-8 -*-
import cv2 #opencv2库,用于进行各种图像处理
import time
import socket #socket库,用于构建tcp/ip 通信
# 服务端ip地址
HOST = '192.168.2.102' #字符串类型存储 host ip,tcp/ip通信服务器需要固定的ip与port
# 服务端端口号
PORT = 8080
ADDRESS = (HOST, PORT) #元组方式存储ip与port
# 创建一个套接字,命名为tcpClient
tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #socket.socket() 函数:socket.AF_INET:基于IPv4 ,socket.SOCK_STREAM对应TCP
# 连接远程ip
#tcpClient.bind(('192.168.3.122', 8080))
tcpClient.connect(ADDRESS) #客户端向服务端发起连接。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误
#cap = cv2.VideoCapture('test1080.mp4') #要发送的视频,如果是0为摄像头
cap = cv2.VideoCapture(0)
while True:
# 计时
start = time.perf_counter() #计时器,第一次调用的时间存储在start里
# 读取图像
ref, cv_image = cap.read() #返回第一个ref为true或false,表示是否读到了图像 ,第二个参数表示截取到一帧的图片数据,是一个三维数组
# 压缩图像
#cv2.imdecode()函数从指定的内存缓存中读取数据,并把数据转换(解码)成图像格式;主要用于从网络传输数据中恢复出图像。
#cv2.imencode()函数是将图片格式转换(编码)成流数据,赋值到内存缓存中;主要用于图像数据格式的压缩,方便网络传输。
img_encode = cv2.imencode('.jpg', cv_image, [cv2.IMWRITE_JPEG_QUALITY, 40])[1] #第一个参数是压缩为什么格式,第二个参数是要压缩的数据源,最后一个参数是解码压缩参数,数字越大图片质量越好
# 转换为字节流
bytedata = img_encode.tostring() #将图像转换为字节流
# 标志数据,包括待发送的字节流长度等数据,用‘,’隔开 ,发送的数据类型是 str
flag_data = (str(len(bytedata))).encode() + ",".encode() + " ".encode()
tcpClient.send(flag_data) #客户端发送标志数据,服务器接收后知晓将要发送数据
# 接收服务端的应答
data = tcpClient.recv(1024) #接收数据,数据以bytes类型返回,bufsize指定要接收的最大数据量为1024字节
if ("ok" == data.decode()): #接收到服务器返回'OK'后,发送全部图片数据,这里用decode进行了解码,因为socket传输字节流,对收到的字节解码才能得到字符串......
# if (data.decode()):
# 服务端已经收到标志数据,开始发送图像字节流数据
tcpClient.send(bytedata)
# 接收服务端的应答
data = tcpClient.recv(1024)
if ("ok" == data.decode()):
# 计算发送完成的延时
print("延时:" + str(int((time.perf_counter() - start) * 1000)) + "ms") #再次调用该计时函数,返回与上一次调用的时间间隔
服务器:
#-*- coding: UTF-8 -*-
import socket
import cv2
import numpy as np
HOST = '192.168.2.102'
PORT = 8080
ADDRESS = (HOST, PORT)
# 创建一个套接字
tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地ip
tcpServer.bind(ADDRESS)
# 开始监听
tcpServer.listen(5)
while True:
print("等待连接……")
client_socket, client_address = tcpServer.accept() #accept() 函数,堵塞等待客户端连接;反回(conn,address)二元元组,其中conn是一个通信对象,可以用来接收和发送数据。address是连接客户端的地址。
print("连接成功!")
try: #使用try expect 便于处理异常
while True:
# 接收标志数据
data = client_socket.recv(1024)
if data: #接收到的不为空就进入
# 通知客户端“已收到标志数据,可以发送图像数据”
client_socket.send(b"ok") #关于b"ok" 看https://www.delftstack.com/zh/howto/python/python-b-in-front-of-string/ ,这里是将ok变为byte
# 处理标志数据
flag = data.decode().split(",") # strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列 ,这里将结尾的 '' 移除
# 图像字节流数据的总长度
total = int(flag[0]) #flag[0]是第一个列表元素,flag是一个列表,返回第一个元素就是数据总长度的字符串形式,再用int()转为整数
# 接收到的数据计数
cnt = 0
# 存放接收到的数据
img_bytes = b""
while cnt < total:
# 当接收到的数据少于数据总长度时,则循环接收图像数据,直到接收完毕
data = client_socket.recv(256000) #一次能接收的最大数据量为256000byte的数据
img_bytes += data #对总数据量计数
cnt += len(data)
print("receive:" + str(cnt) + "/" + flag[0]) #打印接收/需接收
# 通知客户端“已经接收完毕,可以开始下一帧图像的传输”
client_socket.send(b"ok")
# 解析接收到的字节流数据,并显示图像
img = np.asarray(bytearray(img_bytes), dtype="uint8")
img = cv2.imdecode(img, cv2.IMREAD_COLOR)
cv2.namedWindow("img", 0)
cv2.resizeWindow("img", 1280, 720) #重设置显示界面的大小
cv2.imshow("img", img) #第一个参数是窗口的名字,第二个是图像
cv2.waitKey(1)
else:
print("已断开!")
break
finally:
client_socket.close()
python socket 编程实例3——tcp proxy 代理服务器
#https://www.youtube.com/watch?v=iApNzWZG-10
import socket
from threading import Thread
import os
#线程2
class Proxy2Server(Thread):
#首先设置服务器连接(用_init_方法来构造)
#参考https://www.cnblogs.com/ant-colonies/p/6718388.html
def __init__(self, host, port):#如果没有在__init__中初始化对应的实例变量的话,导致后续引用实例变量会出错
super(Proxy2Server,self).__init__()
self.game = None #设置为连接用户的套接字,但是该套接字是由Game2Proxy线程创建的
self.port = port
self.host = host #连接服务器的ip和端口
self.server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.server.connect((host,port))
#在这个线程中执行的函数
def run(self):
#创建一个循环来执行数据处理和网络连接
while True:
data = self.server.recv(4096)#最多接收4k的数据
if data:
#转发所有数据到用户
print("[{}] <- {}") #.format(self.port,data[:100].encode('hex'))#用作测试,可以打印出数据的流向
self.game.sendall(data)
#线程1(监听用户是否与代理服务器连接)
class Game2Proxy(Thread):
def __init__(self,host,port):
super(Game2Proxy,self).__init__()
self.server = None #设置为连接服务器的套接字,但是该套接字是由线程2创建的
self.port = port
self.host = host #连接用户的ip和端口
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind((host,port))
sock.listen(1)#这些都是上面官方文档里面调用的例程实现的
#等待用户的连接
self.game ,addr = sock.accept() #sock.accept接收套接字
#当客户端连接之后我们将获得代理服务器与客户端通信的套接字,并将其分配给self.game,然后在下面的线程中利用永久循环来接收用户端的数据
def run(self):
while True: #死循环接收用户的数据
data = self.game.recv(4096)#最大数据量4k
if data: #如果真的接收到了用户发送过来的数据,那麽我们会尝试将此数据转发到服务器的套接字,即另外一个线程的套接字
#转发给服务器
print("[{}] -> {}") #.format(self.port,data[:100].encode('hex'))#用作测试,可以打印出数据的流向
self.server.sendall(data)
#上面的两个线程创建完毕之后,需要为每一个线程提供对另外一个套接字的引用
#为此,我创建了一个更通用的类,命名为Proxy
class Proxy(Thread):
def __init__(self,from_host,to_host,port):#如果没有在__init__中初始化对应的实例变量的话,导致后续引用实例变量会出错
super(Proxy, self).__init__()
self.from_host = from_host
self.to_host = to_host
self.port = port
def run(self):
while True:
#print ("[proxy({})] setting up")
print ("代理服务器设置完毕,等待设备接入...")
#用户会连接到下面这个
self.g2p = Game2Proxy(self.from_host, self.port) #运行我们创建的这个线程,它等待用户端连接到指定端口
#如果代理服务器与用户建立连接之后,另外一个线程将建立到服务器的转发连接
self.p2s = Proxy2Server(self.to_host, self.port)
#print ("[proxy({})] connection established")
print ("代理服务器已和设备连接,正在传输...")
#现在两个线程都创建了套接字,我们接下来要做的就是交换他们
self.g2p.server = self.p2s.server #将与客户端建立的套接字转发给真实服务器
self.p2s.game = self.g2p.game #将服务器传回的套接字转发到客户端
#线程设置完毕,现在我们来真正启动它
self.g2p.start()
self.p2s.start()
#写到这里的时候,唯一缺少的就是创建一个或多个代理线程,我们先从主服务器开始
master_server = Proxy('0.0.0.0', '192.168.2.222', 5555)
#监听自己所有本机端口3333,并将它转发到真实的服务器ip 192.168.178.54
master_server.start() #启动
#_game_server = Proxy('0.0.0.0', '192.168.2.222', 5555)
#_game_server.start()
'''
#除此之外,客户端想要连接多个服务器的时候,我们可以启动多个代理(多分配几个不同端口即可)
for port in range(3000,3006):
_game_server = Proxy('0.0.0.0','192.168.178.54',port)
_game_server.start()
#写到这里就已经可以工作了
'''
一些项目
python socket实现ftp客户端和服务器收发文件及md5加密文件
python 基于socket与opencv实现视频传输