socket编程
林老师讲socket
netstat -an |grep 8080
监听端口
一 基于TCP的套接字
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字
ss.listen() #监听链接
inf_loop: #服务器无限循环
cs = ss.accept() #接受客户端链接
comm_loop: #通讯循环
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
cs = socket() # 创建客户套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通讯循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户套接字
二 基于UDP的套接字
udp是无连接的,先启动哪一端都不会报错
1 ss = socket() #创建一个服务器的套接字
2 ss.bind() #绑定服务器套接字
3 inf_loop: #服务器无限循环
4 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
5 ss.close() # 关闭服务器套接字
cs = socket() # 创建客户套接字
comm_loop: # 通讯循环
cs.sendto()/cs.recvfrom() # 对话(发送/接收)
cs.close()
三 基于tcp制作一个远程执行命令程序
注意:
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码
且只能从管道里读一次结果
注意:命令ls -l ; lllllll ; pwd
的结果是既有正确stdout结果,又有错误stderr结果
服务端
- from socket import *
- import subprocess
- ip_port = ('127.0.0.1', 8080)
- back_log = 5
- buffer_size = 1024
- tcp_server = socket(AF_INET, SOCK_STREAM) #=> 选择TCP模式
- tcp_server.bind(ip_port) #=> 绑定服务端地址和端口
- tcp_server.listen(back_log) #=> 开始监听
- while True:
- conn, addr = tcp_server.accept()
- print('新的客户端链接', addr)
- while True:
- # 收
- try:
- cmd = conn.recv(buffer_size)
- if not cmd:break
- print('收到客户端的命令', cmd)
- # 执行命令,得到命令的运行结果cmd_res
- #=> 传过来的是字节,需要用decode解码,五个参数
- res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
- stderr=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE)
- err = res.stderr.read() #=> stderr保存的是错误信息
- #=> 如果出错将报错信息赋值给cmd_res,否则将输出内容赋值
- if err:
- cmd_res = err
- else:
- cmd_res = res.stdout.read() #=> stdout保存的是输出信息
- # 发
- if not cmd_res: #=> 如果cmd_res不为空,发送给客户端
- cmd_res = '执行成功'.encode('gbk') #=> 发送时要encode,Windows下中文编码为gbk
- conn.send(cmd_res)
- except Exception as e:
- print(e)
- break
客户端
- from socket import *
- ip_port = ('127.0.0.1', 8080)
- back_log = 5
- buffer_size = 1024
- tcp_client = socket(AF_INET, SOCK_STREAM)
- tcp_client.connect(ip_port) #=> 服务端是绑定ip_port,而客户端是连接,connect
- #=> 用到while True的时候考虑好break和continue的条件
- while True:
- cmd = input('>>:').strip()
- if not cmd:continue
- if cmd == 'quit':break
- tcp_client.send(cmd.encode('utf-8')) #=> 发送时同样要encode
- cmd_res = tcp_client.recv(buffer_size) #=> 客户端、服务端接收时只需要传入buffer_size
- print('命令执行的结果是', cmd_res.decode('gbk')) #=> 收到的数据必须decode,而且要与encode时的编码一致
- tcp_client.close()#=>最后要关闭连接
四 粘包现象
客户端提走的数据,后一次提走的数据可能并不是这次请求所获的数据,而是前一次请求得到数据所剩下的。
只有TCP有粘包现象,UDP永远不会粘包
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
1.TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
五 解决粘包的lowB方法
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
解决粘包-服务端
- from socket import *
- import subprocess
- ip_port = ('127.0.0.1', 8080)
- back_log = 5
- buffer_size = 1024
- tcp_server = socket(AF_INET, SOCK_STREAM) #=> 选择TCP模式
- tcp_server.bind(ip_port) #=> 绑定服务端地址和端口
- tcp_server.listen(back_log) #=> 开始监听
- while True:
- conn, addr = tcp_server.accept()
- print('新的客户端链接', addr)
- while True:
- # 收
- try:
- cmd = conn.recv(buffer_size)
- if not cmd:break
- print('收到客户端的命令', cmd)
- # 执行命令,得到命令的运行结果cmd_res
- #=> 传过来的是字节,需要用decode解码,五个参数
- res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
- stderr=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE)
- err = res.stderr.read() #=> stderr保存的是错误信息
- #=> 如果出错将报错信息赋值给cmd_res,否则将输出内容赋值
- if err:
- cmd_res = err
- else:
- cmd_res = res.stdout.read() #=> stdout保存的是输出信息
- # 发
- if not cmd_res: #=> 如果cmd_res不为空,发送给客户端
- cmd_res = '执行成功'.encode('gbk') #=> 发送时要encode,Windows下中文编码为gbk
- length = len(cmd_res)#=>执行命令后要返回内容的长度
- conn.send(str(length).encode('utf-8'))#=>将长度发送给客户端
- client_ready = conn.recv(buffer_size)#=>用来接收客户端准备好接收的指令
- if client_ready == b'ready':#=>确定客户端准备接收后发送内容
- conn.send(cmd_res)
- except Exception as e:
- print(e)
- break
解决粘包-客户端
- from socket import *
- ip_port = ('127.0.0.1', 8080)
- back_log = 5
- buffer_size = 1024
- tcp_client = socket(AF_INET, SOCK_STREAM)
- tcp_client.connect(ip_port) #=> 服务端是绑定ip_port,而客户端是连接,connect
- #=> 用到while True的时候考虑好break和continue的条件
- while True:
- cmd = input('>>:').strip()
- if not cmd:continue
- if cmd == 'quit':break
- tcp_client.send(cmd.encode('utf-8')) #=> 发送时同样要encode
- # 解决粘包
- length = tcp_client.recv(buffer_size) #=>从服务端接收到返回内容的长度
- print('========>', length)
- tcp_client.send(b'ready')#=>提示服务端客户端已做好接收返回内容的准备
- length = int(length.decode('utf-8'))#=>接收的length是字节,转成int
- recv_size = 0#=>表示已接收的内容长度
- recv_msg = b''
- #=>不断接收内容直到接收完毕
- while recv_size < length:
- recv_msg += tcp_client.recv(buffer_size)#=>将接收到的内容接在后面
- recv_size = len(recv_msg)#=>更新已经接收到的长度
- # cmd_res = tcp_client.recv(buffer_size) #=> 客户端、服务端接收时只需要传入buffer_size
- print('命令执行的结果是', recv_msg.decode('gbk')) #=> 收到的数据必须decode,而且要与encode时的编码一致
- tcp_client.close()#=>最后要关闭连接
为何low:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
六 解决粘包的NB方法
1 方法整体说明
将整体分成两部分,为字节流加上自定义固定长度报头,报头中只包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头(接收固定值的字节流),然后再取真实数据
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
2 用到的知识点
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
利用里面的函数struct.pack(fmt, values)
,第一个参数设置为'i'代表转成int长度的值
struct.pack('i',1111111111111)
![详细对应关系 详细对应关系](https://images2018.cnblogs.com/blog/701977/201806/701977-20180604164329626-924817903.png)
iter()的高级用法
iter(object, sentinel)
第一个参数是要迭代的对象,第二个参数是当输出值为这个参数时停止迭代。
functools中的partial
partial函数用于固定函数执行时的第一个参数
第一个参数为要执行的函数,第二个参数为要传给执行函数的第一个固定参数。
服务端
- from socket import *
- import subprocess
- import struct
- ip_port = ('127.0.0.1', 8080)
- back_log = 5
- buffer_size = 1024
- tcp_server = socket(AF_INET, SOCK_STREAM) #=> 选择TCP模式
- tcp_server.bind(ip_port) #=> 绑定服务端地址和端口
- tcp_server.listen(back_log) #=> 开始监听
- while True:
- conn, addr = tcp_server.accept()
- print('新的客户端链接', addr)
- while True:
- # 收
- try:
- cmd = conn.recv(buffer_size)
- if not cmd:break
- print('收到客户端的命令', cmd)
- # 执行命令,得到命令的运行结果cmd_res
- #=> 传过来的是字节,需要用decode解码,五个参数
- res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
- stderr=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE)
- err = res.stderr.read() #=> stderr保存的是错误信息
- #=> 如果出错将报错信息赋值给cmd_res,否则将输出内容赋值
- if err:
- cmd_res = err
- else:
- cmd_res = res.stdout.read() #=> stdout保存的是输出信息
- # 发
- if not cmd_res: #=> 如果cmd_res不为空,发送给客户端
- cmd_res = '执行成功'.encode('gbk') #=> 发送时要encode,Windows下中文编码为gbk
- length = len(cmd_res)#=>执行命令后要返回内容的长度
- # conn.send(str(length).encode('utf-8'))#=>将长度发送给客户端
- # client_ready = conn.recv(buffer_size)#=>用来接收客户端准备好接收的指令
- # if client_ready == b'ready':#=>确定客户端准备接收后发送内容
- data_length = struct.pack('i', length)#=>将返回内容的长度打包成int的长度,即4bytes
- conn.send(data_length)
- conn.send(cmd_res)
- except Exception as e:
- print(e)
- break
客户端
- from socket import *
- import struct
- from functools import partial
- ip_port = ('127.0.0.1', 8080)
- back_log = 5
- buffer_size = 1024
- tcp_client = socket(AF_INET, SOCK_STREAM)
- tcp_client.connect(ip_port) #=> 服务端是绑定ip_port,而客户端是连接,connect
- #=> 用到while True的时候考虑好break和continue的条件
- while True:
- cmd = input('>>:').strip()
- if not cmd:continue
- if cmd == 'quit':break
- tcp_client.send(cmd.encode('utf-8')) #=> 发送时同样要encode
- # 解决粘包
- # length = tcp_client.recv(buffer_size) #=>从服务端接收到返回内容的长度
- # tcp_client.send(b'ready')#=>提示服务端客户端已做好接收返回内容的准备
- # length = int(length.decode('utf-8'))#=>接收的length是字节,转成int
- # recv_size = 0#=>表示已接收的内容长度
- # recv_msg = b''
- # #=>不断接收内容直到接收完毕
- # while recv_size < length:
- # recv_msg += tcp_client.recv(buffer_size)#=>将接收到的内容接在后面
- # recv_size = len(recv_msg)#=>更新已经接收到的长度
- # cmd_res = tcp_client.recv(buffer_size) #=> 客户端、服务端接收时只需要传入buffer_size
- #新解决粘包方法
- length_data = tcp_client.recv(4)#=>先开始接收4个字节,这4个字节肯定是内容长度信息
- length = struct.unpack('i', length_data)[0]#=>解压之后是个tuple,第一项是值
- # =>jion函数可以作用于迭代器,将多次接收的信息连接到一起,相当于for循环
- recv_msg = ''.join(iter(partial(tcp_client.recv, buffer_size), b''))
- print('命令执行的结果是', recv_msg.decode('gbk')) #=> 收到的数据必须decode,而且要与encode时的编码一致
- tcp_client.close()#=>最后要关闭连接
七 认证客户端链接的合法性
利用hmac+加盐的方式来实现
客户端
- #!/usr/bin/env python
- #-*- coding:utf-8 -*-
- """
- @author:BanShaohuan
- @file: server_合法.py
- @time: 2018/06/04
- @contact: banshaohuan@163.com
- @software: PyCharm
- """
- from socket import *
- import hmac,os
- secret_key=b'banshaohuan'
- 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',9999)
- bufsize=1024
- server_handler(ip_port,bufsize)
合法客户端
- #!/usr/bin/env python
- #-*- coding:utf-8 -*-
- """
- @author:BanShaohuan
- @file: client_合法.py
- @time: 2018/06/04
- @contact: banshaohuan@163.com
- @software: PyCharm
- """
- #_*_coding:utf-8_*_
- from socket import *
- import hmac,os
- secret_key=b'banshaohuan'
- 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',9999)
- bufsize=1024
- client_handler(ip_port,bufsize)
客户端非法:不知道加密方式
- #!/usr/bin/env python
- #-*- coding:utf-8 -*-
- """
- @author:BanShaohuan
- @file: client_非法.py
- @time: 2018/06/04
- @contact: banshaohuan@163.com
- @software: PyCharm
- """
- #_*_coding:utf-8_*_
- 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',9999)
- bufsize=1024
- client_handler(ip_port,bufsize)
客户端非法:不知道secret_key
- #!/usr/bin/env python
- #-*- coding:utf-8 -*-
- """
- @author:BanShaohuan
- @file: client非法_2.py
- @time: 2018/06/04
- @contact: banshaohuan@163.com
- @software: PyCharm
- """
- #_*_coding:utf-8_*_
- from socket import *
- import hmac,os
- secret_key=b'banshaohuan111'
- 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',9999)
- bufsize=1024
- client_handler(ip_port,bufsize)
![](https://img2018.cnblogs.com/blog/701977/201904/701977-20190427174556211-1777449894.png)