赞
踩
学习了大佬的知识,简单记一些笔记
https://www.jianshu.com/p/066d99da7cbd
http://c.biancheng.net/view/2351.html
在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。
我的理解就是Socket就是该模式的一个实现:即socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
百度百科:套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合
在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
你也许听很多高手说过,UNIX/Linux 中的一切都是文件!那个家伙说的没错。
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
请注意,网络连接也是一个文件,它也有文件描述符!你必须理解这句话。
我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
用 read() 读取从远程计算机传来的数据;
用 write() 向远程计算机写入数据。
你看,只要用 socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!
Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。因此,本教程如果涉及 Windows 平台将使用“句柄”,如果涉及 Linux 平台则使用“描述符”。
与 UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专门针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了
a、消息传递(管道、消息队列、FIFO)
b、同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
c、共享内存(匿名的和具名的,eg:channel)
d、远程过程调用(RPC)
我们要理解网络中进程如何通信,得解决两个问题:
a、我们要如何标识一台主机,即怎样确定我们将要通信的进程是在那一台主机上运行。
b、我们要如何标识唯一进程,本地通过pid标识,网络中应该怎样标识?
解决办法:
a、TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机
b、传输层的“协议+端口”可以唯一标识主机中的应用程序(进程),因此,我们利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互
网络中进程间利用三元组【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的功能简化为三个:建立连接、发送数据以及接收数据,下链接为建立连接的流程
http://c.biancheng.net/view/2351.html
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发
送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取
这些I/O缓冲区特性可整理如下:
一般情况下,不用关系默认缓冲区的大小,但也可以用如下代码查看与修改
sock.getsockopt() # 获取缓冲区大小
sock.setsockopt() #更改缓冲区大小
收发函数特性:
recv特征:
如果建立的另一端链接被断开, 则recv立即返回空字符串
recv是从接受缓冲区取出内容,当缓冲区为空则阻塞
recv如果一次接受不完缓冲区的内容,下次执行会自动接受
send特征:
如果发送的另一端不存在则会产生Pipe Broken异常
send是从发送缓冲区发送内容,当缓冲区为满则堵塞
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性,TCP套接字默认情况下是阻塞模式,也是最常用的。
socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。
例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。
假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。
这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
close方法可以释放一个连接的资源,但是不是立即释放,如果想立即释放,那么请在close之前使用shutdown方法
shutdown方法是用来实现通信模式的,模式分三种,SHUT_RD 关闭接收消息通道,SHUT_WR 关闭发送消息通道,SHUT_RDWR 两个通道都关闭
也就是说,想要关闭一个连接,首先把通道全部关闭,然后在release连接,以上三个静态变量分别对应数字常量:0,1,2
self.tcpClient.shutdown(2) #关闭消息发送通道
self.tcpClient.close() #关闭套接字连接
客户端:
#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!')
客户端:
#-*- 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()
#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() #写到这里就已经可以工作了 '''
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。