什么是socket?socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口的后面,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议;所以一般情况下我们无需深入理解TCP/IP协议,因为socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循TCP/UDP标准的。
也有人将socket说成ip + port,ip是用来标识互联网中一台主机的位置,而port是用来标识这台主机上的应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序,而程序的pid是同一台机器上不同进程或者线程的标识。
套接字有两种,一种是基于文件类型的套接字家族--AF_UNIX,另一种是基于网络类型的套接字家族--AF_INET。
那么socket的工作流程是什么呢?我们可以从服务端说起,服务端先初始化socket对象,然后与接口绑定,并对端口进行监听,然后调用accept阻塞(socket是一种IO阻塞,后面我们将会讲到),等待客户端连接。如:
import socket # 创建socket对象 第一个参数什么是哪一种socket 第二个参数表示是基于样的数据 我们以SOCK_STREAM的形式进行创建 也就是TCP 需要注意的就是既然我们以流的形式和网络的形式,那么底层传输数据就是二进制的形式,所以在获取数据的时候,我们有必要对二进制进行编码和解码 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 绑定ip和端口 以元组的形式 sk.bind(("127.0.0.1",8080)) # 对端口进行监听 如果传递参数,就表示同时监听多少条数据 sk.listen(2) # 等待客户端发起连接 返回的是两个参数 一个是客户端链接 一个地址 conn,addr = sk.accept() # 接收数据 参数表示接收数据的大小 data = conn.recv(1024) # 查看客户端发来的数据 print(data.decode("utf-8")) # socket是一种一接一发 的这种模式,所以这里我们可以可客户端回消息 conn.send("msg is recv".encode("utf-8")) # 通信结束 conn.close() sk.close()
那么客户端呢?客户端我们同样也是需要创建socket对象的,在创建完对象之后,就发起连接然后发送消息,如:
import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 创建连接 也就是一开始就创建链接来连接服务端 sk.connect(("127.0.0.1",8080)) # 向服务端发起数据 sk.send("my first c/s".encode("utf-8")) # 接收服务端发来的消息 可以说是确认服务端那边是否已经接收到 data = sk.recv(1024) print("msg from server:",data.decode("utf-8")) sk.close()
接下来,我们先看一下套接字都有哪些方法呢?
recv(buffersize[,flag]):接收TCP套接字的数据,buffersize表示接收的最大数据量,flag提供的是有关信息的其他消息,一般可以忽略
send(data[,flags]):将data中的数据发送到socket,send在待发送数据量大于己端缓存区间剩余值时,数据丢失,不会发完
sendall(data[,flags]):完整的发送data数据,本子就是循环调用send,sendall在待发送数据大于己端缓存区剩余空间时,数据不丢失,因为它会循环调用send直到发完
recvfrom(buffersize[,flags]:接收UDP数据,但是返回值是(data,addr)
snedto(data[,flags],addr):发送UDP数据,第二个参数指定要发送到地址
getpeername():连接到当前套接字远端的地址
getsockname():当前套接字的地址
getsockopt():返回指定套接字的参数
setsockopt():设置套接字参数
setblocking(flag):设置阻塞和非阻塞模型,默认是阻塞也就是flag为True,当设置为False表示非阻塞
settimeout(timeout):设置套接字超时时间,timeout是一个浮点数,值为None表示没有超时时期,一般超时时期应该在刚创建套接字时创建,因为它们可能用于连接操作(比如connect)
gettimeout():获取超时时间,单位是秒,如果没有设置timeout的话,则返回的是None
fileno():返回套接字的文件描述符
makefile():创建一个与该套接字相关联的文件
案例:基于TCP的套接字通信
# 服务端
from socket import * import random # 创建套接字对象 s_server = socket(AF_INET,SOCK_STREAM) s_server.bind(("127.0.0.1",8080)) s_server.listen(5) msgs = ["问题正在处理呢","此刻排队人数太多,请稍等","请问有什么可以帮您的","小白正在想办法帮您解决问题呢"] # 让客服端循环去监听客户端发来的消息 while True: conn,addr = s_server.accept() # 不断的去处理conn里面的消息 while True: try: data = conn.recv(1024) print("客户来信:",data.decode("utf-8")) index = random.randrange(4) conn.send(msgs[index].encode("utf-8")) except Exception as e: print(e) break s_server.close()
# 客户端
from socket import * # 创建套接字对象,向服务端发送消息 c_socket = socket(AF_INET,SOCK_STREAM) c_socket.connect(("127.0.0.1",8080)) # 可以不断的去发送消息 while True: msg = input("请输入您想说的话:>>>") c_socket.send(msg.encode("utf-8")) # 接收客户端的回复 data = c_socket.recv(1024) print("客服:",data.decode("utf-8")) if msg == "end": break c_socket.close()
那关于UDP的工作流程呢?服务端这边,同样的也是创建套接字对象,然后绑定套接字,与TCP不同的是通过recvfrom来接收数据,sendto来发送数据,客户端创建套接字对象之后,直接通过sendto来发送数据,客户端和服务端在sendto中都要携带地址参数
案例:通过UDP来实现聊天功能
# UDP服务端
from socket import * # 创建UDP套接字 s_socket = socket(AF_INET,SOCK_DGRAM) s_socket.bind(("127.0.0.1",8080)) # UDP不需要监听 # 这里也同样的不断的去接收数据 while True: # data里面包含了(msg,addr) msg,addr = s_socket.recvfrom(1024) # 接收消息 print("傻蛋:",msg.decode("utf-8")) # 回复消息 s_socket.sendto(msg,addr)
# UDP客户端
from socket import * c_socket = socket(AF_INET,SOCK_DGRAM) while True: msg = input("请输入内容>>>:") c_socket.sendto(msg.encode("utf-8"),("127.0.0.1",8080)) # 接收服务端发来的消息 data,addr = c_socket.recvfrom(1024) print("客服:",data.decode("utf-8"))
上面我们通过两个案例展示了基于TCP和UDP的两种通信模式,那么这两种又什么区别呢?其实在我们进行TCP通信的时候,如果我们多开了几个客户端,将会发现,必须要的到前一个通信结束,才会轮到自己,而UDP不管开多少个客户端,都能正常通信,换一句话说就是:TCP是不能实现并发的,而UDP是可以实现并发,的,当然,如果想要TCP也能实现并发,那就需要特殊的去处理了。
现在,我们大概也知道socket的通信流程了,接下来,再看一些概念性的东西。
基于TCP 的SYN洪水攻击,那么什么是SYN洪水攻击呢?在我们建立socket通信的是的时候,对于TCP来说,它是一直处于等待连接的状态,当客户端连接成功和获取客户端消息之后,需要给客户端发送消息,也就是一收一发的这种状态,它将会等待客户端那边收到消息的确认信息,如果没有收到客户端那边的信息,就会处于卡顿的状态,基于这种卡顿,等待,就可以发送大量伪造的TCP连接请求,这种请求发送完了就完了,然后服务端就处在一直等待,使得服务端这边资源在不断的被消耗,严重的服务端的资源会被耗尽。
基于TCP的socket可靠性在哪?在我们建立连接三次握手成功之后,它的可靠性主要体现在之后的的数据传输,因为客户端在发送数据的时候,如果服务端收到消息,需要给客户端一个回应,如果这个时候,客户端没有收到回应,将会重新发送消息。
为什么三次握手四次挥手呢?三次握手,实际也是四次,只是在握手的时候,没有涉及到数据的传输,所以就把SYN seq = y 和 ACK=x + 1合成了一次,但是断开连接就不能合并了,因为断开连接已经涉及到数据的连接,如果在这个时候,客户端在发送断开请求的时候,将服务端的回应合并了,那么客户端就直接结束了,但是服务端这个时候,还有可能数据没有传输完成,就会造成数据的丢失了,所以断开必须要四次才能完成。
如果在大并发的情况下,怎么处理连接呢?在大并发的情况下,服务端会主动的断开连接,变成TIME_WAIT状态,为了节省资源,而不会去保护客户端的链接状态。
在收发消息的时候,是用户态和内核态的切换,那什么是用户态和内核态,它们之间又是怎么工作的?内核态是操作系统内核的运行模式,运行在该模块的代码,可以无限制的对系统存储,外部设备进行访问,也可以称为特权态,比如CPU可以访问内存所有数据,包括硬盘,网卡等;用户态,我们可以称为一种非特权状态,执行的代码被硬件限定,不能进行某些操作,比如写入其他进程的存储空间,以防止给操作系统带来安全隐患,每个进程都在自己的用户空间中运行,而不允许存取其它程序的用户空间。简单地说,就是所有的用户程序都是运行在用户态,有的时候,需要读取数据的话,就通过内核态来完成了,流程如下:
①用户态将一些数据存放在寄存器中,以此表明需要操作系统提供服务
②用户态执行陷阱指令(系统调用)
③CPU切换到内核态,并跳到位于内存指定位置的指令,这些指令是操作系统的一部分,它们具有内存保护,不可被用户态程序所访问
④这些指令称为陷阱或者系统调用处理器,它们会读取程序放入内存的数据参数,并执行程序请求服务
⑤系统调用完成之后,操作会重置CPU为用户态并返回系统调用的结果
所以,我们在收消息发消息的这些数据是在内核态完成的
recv和recvfrom都是收消息,那这两者有什么区别呢?对于TCP来说,如果收消息缓冲区的数据为空,就会阻塞,由于TCP是基于链接的,如果一端断开了,另一端也跟着完蛋;而对于UDP来说,如果收消息的缓冲区为"空"(实际不为空,即使没有消息内容,但是还有一个报头),recvfrom不会阻塞,但recvfrom收到数据小于sendto发来的数据的时候,数据就会丢失,同时,如果只sendto数据,不通过recvfrom来接受数据的话,数据也会丢失,而对于,TCP数据不会丢失,会在自己的缓冲区里,但是会造成粘包,下面我们会讲到什么是粘包。
案例:基于UDP实现一个时间服务器
# server端
from socket import * from time import strftime ip_port = ("127.0.0.1",8080) buffersize = 1024 s_sk = socket(AF_INET,SOCK_DGRAM) s_sk.bind(ip_port) while True: data,addr = s_sk.recvfrom(buffersize) print("想要的时间格式: ",data.decode("utf-8")) # 判断data是否为空 如果为空就发送一种默认的时间格式 if not data: time_fmt = "%Y-%m-%d %X" else: time_fmt = data.decode("utf-8") # 将日期格式返回 back_time = strftime(time_fmt) s_sk.sendto(back_time.encode("utf-8"),addr) s_sk.close()
# client 端
from socket import * ip_port = ("127.0.0.1",8080) buffersize = 1024 c_sk = socket(AF_INET,SOCK_DGRAM) while True: time_fmt = input("请输入时间格式:(例如'%Y %m %d %X')>>>") c_sk.sendto(time_fmt.encode("utf-8"),ip_port) # 接收服务端返回的时间 time_fmt = c_sk.recv(buffersize) print("现在时间是: ",time_fmt.decode("utf-8")) c_sk.close()
我们说,TCP当recv的数据小于send过来的数据时,不会丢失数据,但是会粘包,那么什么是粘包呢?
粘包,就是当从缓冲区获取数据的时候,很多条数据混在了一起,这就是粘包,先来看一个粘包的案例:
# server 端
from socket import * import subprocess ip_port = ("127.0.0.1",8081) buffersize = 1024 s_sk = socket(AF_INET,SOCK_STREAM) s_sk.bind(ip_port) s_sk.listen(5) while True: conn,addr = s_sk.accept() while True: try: data = conn.recv(buffersize) # 客户端需要执行的命令 print("客户端需要执行的命令: ",data.decode("utf-8")) # 判断命令的可用性 命令为空的情况 if len(data) == 0: break # subprocess # 通过shell来解释命令 act_res = subprocess.Popen(data.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) # 如果命令解析错误 act_err = act_res.stderr.read() if act_err: ret = act_err else: ret = act_res.stdout.read() conn.sendall(ret) except Exception as e: print(e) break conn.close() s_sk.close()
# client 端
from socket import * ip_port = ("127.0.0.1",8081) buffersize = 1024 c_sk = socket(AF_INET,SOCK_STREAM) c_sk.connect(ip_port) while True: cmd = input("请输入您要执行的命令>>>").strip() # 判断输入的数据是否为空,为空的时候继续 if len(cmd) == 0: continue if cmd == "quit": break c_sk.send(cmd.encode("utf-8")) rec_cmd = c_sk.recv(buffersize) print("执行的结果是:",rec_cmd.decode("utf-8")) c_sk.close()
分析一下粘包的来源,发送端可以1k的不断向接受端发送数据,而接收端可以2k或者3k又或者几个字节的去提取数据,也就是说应用程序看到的是一个整体,或者说是一个流,一条消息有多少字节,多应用程序来说是不可见的,因此TCP协议是面向流协议,所以就会容易出现粘包了,而UDP面向的是消息协议,每个UDP都是一个消息段,应用程序必须以消息为单位来提取数据,不能一次性提取任意字节的数据,所以UDP不会产生粘包。举个例子,比如基于TCP的客户端要向服务端上传一个文件,发送文件内容的时候是按照一段一段的字节流发送的,在接收方看来,根本不知道这些字节流从何处开始,到何处结束。
那么造成粘包的最大的原因就是,接收方不知道消息的界限,不知道一次提取多少字节造成的,还有一个原因就是由TCP本身造成的,为了提高效率,发送方往往要接受足够多的数据才发送一个TCP段,若连续几次send的数据都很小,通常TCP会通过优化算法把这些数据合成一个TCP段后一次性发送,这样接收方收到的数据就是粘包数据。
既然粘包是由于不知道数据的大小产生的,那么解决方案就是让双方知道数据的大小;为字节流加上自定义固定长度报头,报头中包含字节流的长度,然后依次send到对端,对端在接受时,先从缓存中取出定长的报头,然后再取真实的数据。需要引入模块,struct模块,它可以把一个类型,如数字,转成固定长度的bytes,步骤如下:
# 假设客户端要上传的文件为1T:1073741824000的文件a.MP4 import json,struct # 为了避免粘包,必须自定制报头 header={ "file_size":1073741824000, "file_name":"a.MP4", "md5":"'8f6fbf8347faa4924a76856701edb0f3" } # 为了该报头能传送,需要序列化,需要序列化并且转为bytes head_bytes = bytes(json.dumps(header),encoding="utf-8") # 为了让客户端知道报头的长度,用struct将报头长度的这个数字转化成固定长度:4个字节 # 这4个字节只包含一个数字,就是报头的长度 head_len_bytes = struct.pack("i",len(head_bytes)) # 客户端开始发送 c_sk.send(head_len_bytes) # 先发送报头长度 4个字节 c_sk.send(head_bytes) # 再发送报头的字节格式 c_sk.sendall("文件内容") # 然后发送真实内容的字节格式 # 服务端接收数据 head_len_bytes = s_sk.recv(4) # 先接收4个字节,得到报头长度的字节格式 # 提取报头的长度 x = struct.unpack("i",head_len_bytes)[0] # 按照报头长度x,收取报头的bytes格式 head_bytes = s_sk.recv(x) # 提取报头 header = json.loads(json.dumps(header)) # 根据报头内容提取真实的数据 real_data_len = s_sk.recv(header["file_size"]) s_sk.recv(real_data_len)
案例:解决粘包
# server 端
import socket,subprocess,struct,json ip_port = ("127.0.0.1",8080) s_sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 为socket设置选项 允许地址重用 s_sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) s_sk.bind(ip_port) s_sk.listen(5) while True: conn,addr = s_sk.accept() while True: try: msg = conn.recv(1024) if not msg: break res = subprocess.Popen(msg.decode("utf-8"), shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) res_err = res.stderr.read() if res_err: ret = res_err else: ret = res.stdout.read() # 定义报头 headers = {"data_size":len(ret)} # 序列化 head_json = json.dumps(headers) # 发送 # 先发报头长度 conn.send(struct.pack("i",len(head_json))) # 再发报头 conn.send(head_json.encode("utf-8")) # 发真实内容 conn.sendall(ret) except Exception as e: print(e) break conn.close() s_sk.close()
# client 端
import socket,struct,json ip_port = ("127.0.0.1",8080) c_sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) c_sk.connect(ip_port) while True: cmd = input("请输入要执行的命令>>>") if len(cmd) == 0: continue if cmd == "quit": break c_sk.send(cmd.encode("utf-8")) # 接收4个字节的报头 head = c_sk.recv(4) # 拆包 head_json_len = struct.unpack("i",head)[0] head_json = json.loads(c_sk.recv(head_json_len).decode("utf-8")) data_len = head_json["data_size"] recv_size = 0 recv_data = b'' while recv_size < data_len: recv_data += c_sk.recv(1024) recv_size += len(recv_data) print(recv_data.decode("utf-8")) c_sk.close()
有的时候,我们希望socket能够实现,比如,文件的上传和下载,可以通过socketserver这个模块来实现。
socketserver相关知识:
# 可供使用的类 ''' BaseServer:包含服务器的核心功能与混合(mix-in)类挂钩,这个类主要使用派生的,所以不会生成这个类的实例 TCPServer/UDPServer:基于网络同步TCP/UDP服务器 UnixStreamServer/UnixDatagramServer:基于文件同步的TCP/UDP服务器 ForkingMixIn/ThreadingMixIn:实现了核心的进程化或线程化功能,作为混合类与服务器一并使用以提供一些异步特性,这个类也不会直接实例化 ForkingTCPServer/ForkingUDPServer:ForkingMixIn与TCPServer/UDPServer的组合 BaseRequestHandler:包含处理服务请求的核心功能,这个类也是用于派生,一般也不会生成实例 StreamRequestHandler/DatagramRequestHandler:用于TCP/UDP服务的处理工具 '''
案例:socketserver实现并发并且无阻塞
# server 端
import socketserver ip_port = ("127.0.0.1",8080) class MyServer(socketserver.BaseRequestHandler): def handle(self): # 对TCP来说request相当于conn,但是对于UDP来说,它是一个元组的形式:(client_data_bytes,udp的套接字对象) print(self.request) # 相当于addr print(self.client_address) # 循环接收消息 while True: try: data = self.request.recv(1024) if not data: break print("1号VIP:",data.decode("utf-8")) self.request.sendall("服务正忙,请稍后...".encode("utf-8")) except Exception as e: print(e) break if __name__ == '__main__': s = socketserver.ThreadingTCPServer(ip_port,MyServer) # 开启永远服务 s.serve_forever()
from socket import * ip_port = ("127.0.0.1",8080) buffersize = 1024 c = socket(AF_INET,SOCK_STREAM) c.connect(ip_port) while True: msg = input("请输入您的问题>>>") if len(msg) == 0 : continue if msg == "quit" : break c.send(msg.encode("utf-8")) msg = c.recv(buffersize) print("美女客服:",msg.decode("utf-8")) c.close()
案例:验证客户端的合法性
# 通过hmac+加盐的方式来实现 from socket import * import hmac,os secret_key=b'is key' def conn_auth(conn): ''' 认证客户端链接 :param conn: :return: ''' print('开始验证新链接的合法性') msg=os.urandom(32) conn.sendall(msg) h=hmac.new(secret_key,msg) digest=h.digest() respone=conn.recv(len(digest)) return hmac.compare_digest(respone,digest) def data_handler(conn,bufsize=1024): if not conn_auth(conn): print('该链接不合法,关闭') conn.close() return print('链接合法,开始通信') while True: data=conn.recv(bufsize) if not data:break conn.sendall(data.upper()) def server_handler(ip_port,bufsize,backlog=5): ''' 只处理链接 :param ip_port: :return: ''' tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(backlog) while True: conn,addr=tcp_socket_server.accept() print('新连接[%s:%s]' %(addr[0],addr[1])) data_handler(conn,bufsize) if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 server_handler(ip_port,bufsize)
# 合法的客户端
from socket import * import hmac secret_key=b'is key' def conn_auth(conn): ''' 验证客户端到服务器的链接 :param conn: :return: ''' msg=conn.recv(32) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest) def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True: data=input('>>: ').strip() if not data:continue if data == 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize)
# 不合法的客户端 不知道加密方式
from socket import * def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) while True: data=input('>>: ').strip() if not data:continue if data == 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize)
# 不合法的客户端 不知道密钥
from socket import * import hmac
# 密钥错误 secret_key=b'is a key' def conn_auth(conn): ''' 验证客户端到服务器的链接 :param conn: :return: ''' msg=conn.recv(32) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest) def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True: data=input('>>: ').strip() if not data:continue if data == 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize)
浙公网安备 33010602011771号