python-网络编程(socket)
一、网络协议osi七层
举例
班级找人
二、传输协议tcp
参考:
https://www.cnblogs.com/linhaifeng/articles/5937962.html
https://blog.csdn.net/liangyf33/article/details/81315864

四、socket概念
五、套接字概念
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
补充:也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序,而程序的pid是同一台机器上不同进程或者线程的标识

五、基于tcp协议的套接字编程
一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。
生活中的场景就解释了套接字的工作原理

实现tcp收到数据的基本步骤
tcp服务端
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字
ss.listen() #监听链接
inf_loop: #服务器无限循环
cs = ss.accept() #接受客户端链接
comm_loop: #通讯循环
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
tcp客户端
cs = socket() # 创建客户套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通讯循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户套接字
1、简单实例
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建实例 买电话,socket.AF_INET:基于网络的套接字;socket.SOCK_STREAM:网络协议为tcp phone.bind(('127.0.0.1', 8000)) # 绑定服务器地址及端口 绑定手机卡 phone.listen(5) # 开机 print("==" * 40) conn, addr = phone.accept() # 等电话,创建连接 msg = conn.recv(1024) # 收信息 print("服务端收到的信息是%s" % msg) conn.send(msg.upper()) # 发信息 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("客户端接收到的信息%s" % data)
2、实现一个客户端循环收发信息
from socket import * ip_port=('127.0.0.1',8000) back_log=5 buffer_size=1024 tcp_server = socket(AF_INET, SOCK_STREAM) # socket.AF_INET:套接字家族;socket.SOCK_STREAM:网络协议为tcp tcp_server.bind(ip_port) # 绑定服务器地址及端口 tcp_server.listen(back_log) # 5:backlog 半连接池,所有客户端请求先放到连接池,然后从连接池里取,逐个处理 print("服务端开始运行") conn, addr = tcp_server.accept() # 创建连接 (三次握手,拿到一个tcp链接) while True: # 循环接收同一个客户端的多条收发信息请求 msg = conn.recv(buffer_size) # 收信息 print("服务端收到的信息是%s" % msg.decode("utf-8")) conn.send(msg.upper()) # 发信息 conn.close() # (四次挥手,断开tcp链接) tcp_server.close()
from socket import * ip_port=('127.0.0.1',8000) buffer_size=1024 tcp_client = socket(AF_INET, SOCK_STREAM) # 拿起手机 tcp_client.connect(ip_port) # 拨打对方号码 while True: msg = input("输入你要发送给服务端的内容:") tcp_client.send(msg.encode('utf-8')) # 网络传输的是二进制类型,需要编码 data = tcp_client.recv(buffer_size) # 接收到服务端的返回信息 print("客户端接收到的信息%s" % data) tcp_client.close()
3、实现服务端可以循环连接多个客户端
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_server = socket(AF_INET, SOCK_STREAM) # socket.AF_INET:套接字家族;socket.SOCK_STREAM:网络协议为tcp tcp_server.bind(ip_port) # 绑定服务器地址及端口 tcp_server.listen(back_log) # 5:backlog 半连接池,所有客户端请求先放到连接池,然后从连接池里取,逐个处理 while True: # 循环接收多个客户端的连接 print("服务端开始运行") conn, addr = tcp_server.accept() # 创建连接 (三次握手,拿到一个tcp链接) while True: # 循环接收同一个客户端的多条收发信息请求 try: msg = conn.recv(buffer_size) # 收信息 print("服务端收到的信息是%s" % msg.decode("utf-8")) conn.send(msg.upper()) # 发信息 except Exception: break # 异常断开当前连接,从连接池里拿连接,创建新的连接,实现可以接收多个客户端连接请求 conn.close() # (四次挥手,断开tcp链接) tcp_server.close()
from socket import * ip_port=('127.0.0.1',8000) buffer_size=1024 tcp_client = socket(AF_INET, SOCK_STREAM) # 拿起手机 tcp_client.connect(ip_port) # 拨打对方号码 while True: msg = input("输入你要发送给服务端的内容:") tcp_client.send(msg.encode('utf-8')) # 网络传输的是二进制类型,需要编码 data = tcp_client.recv(buffer_size) # 接收到服务端的返回信息 print("客户端接收到的信息%s" % data) tcp_client.close()
在linux系统中,客户端断开,服务端不报异常,而是接收到空数据。所以我们用if 判断,如果为空,则退出当前循环
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_server = socket(AF_INET, SOCK_STREAM) # socket.AF_INET:套接字家族;socket.SOCK_STREAM:网络协议为tcp tcp_server.bind(ip_port) # 绑定服务器地址及端口 tcp_server.listen(back_log) # 5:backlog 半连接池,所有客户端请求先放到连接池,然后从连接池里取,逐个处理 while True: # 循环接收多个客户端的连接 print("服务端开始运行") conn, addr = tcp_server.accept() # 创建连接 (三次握手,拿到一个tcp链接) while True: # 循环接收同一个客户端的多条收发信息请求 # try: msg = conn.recv(buffer_size) # 收信息 if not msg: break print("服务端收到的信息是%s" % msg.decode("utf-8")) conn.send(msg.upper()) # 发信息 # except Exception: # break # 异常断开当前连接,从连接池里拿连接,创建新的连接,实现可以接收多个客户端连接请求 conn.close() # (四次挥手,断开tcp链接) tcp_server.close()
4、重启服务端地址被占用问题
在linux系统中,重启服务端有时会报

这是由于在断开连接时,没有完整走完四次挥手流程,使用 netstat -an|grep 端口,可以看到存在FIN_WAIT2状态的记录
解决办法一、
加入一条socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))
办法二、修改linux内核参数
vi /etc/sysctl.conf 编辑文件,加入以下内容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 然后执行 /sbin/sysctl -p 让参数生效。 net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
六、基于UDP协议的套接字编程
实现udp协议收发数据的基本步骤
···udp服务端
ss = socket() #创建一个服务器的套接字
ss.bind() #绑定服务器套接字
inf_loop: #服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
ss.close() # 关闭服务器套接字
···udp客户端
cs = socket() # 创建客户套接字
comm_loop: # 通讯循环
cs.sendto()/cs.recvfrom() # 对话(发送/接收)
cs.close() # 关闭客户套接字
1、简单实例
from socket import * ip_port=("127.0.0.1",8001) buffer_size = 1024 udp_server = socket(AF_INET,SOCK_DGRAM) # 创建一个udp协议的套接字 udp_server.bind(ip_port) # 绑定服务器地址 # 循环接收发送数据 while True: data,addr = udp_server.recvfrom(buffer_size) # 接收客户端发来的数据 print("接收到客户端发来的数据:%s" %data) print(addr) udp_server.sendto(data.upper(),addr) # 发送数据给客户端
from socket import * ip_port = ("127.0.0.1", 8001) buffer_size = 1024 udp_client = socket(AF_INET, SOCK_DGRAM) # 创建一个udp协议的套接字 # 循环发送接收数据 while True: msg = input("请输入发送内容:") udp_client.sendto(msg.encode("utf-8"), ip_port) # 发送给服务端 data, addr = udp_client.recvfrom(buffer_size) # 接收服务端发来的数据 print("接收到服务端发来的数据:", data)
2、基于udp协议实现时间同步
import time from socket import * ip_port = ("127.0.0.1", 8002) buffer_size = 1024 ntp_server = socket(AF_INET, SOCK_DGRAM) ntp_server.bind(ip_port) ntp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) while True: data, addr = ntp_server.recvfrom(buffer_size) # 接收客户端发来的时间格式 if not data: time_format = "%Y-%m-%d %X" # 设置默认格式 else: time_format = data.decode("utf-8") # 解码收到的时间格式 back_time = time.strftime(time_format) ntp_server.sendto(back_time.encode("utf-8"), addr) # 返回当前服务器的时间
from socket import * ip_port = ("127.0.0.1", 8002) buffer_size = 1024 ntp_server = socket(AF_INET, SOCK_DGRAM) while True: msg = input("请输入校准时间格式:") ntp_server.sendto(msg.encode("utf-8"), ip_port) # 发送校准时间的格式 data, addr = ntp_server.recvfrom(buffer_size) # 接收服务器返回的时间格式 print("标准时间:", data.decode("utf-8"))
3、基于tcp实现远程执行命令
import subprocess from socket import * ip_port = ("127.0.0.1", 8000) 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() try: cmd = conn.recv(buffer_size).decode("utf-8") if not cmd: break print("接收到命令", cmd) res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) err_messge = res.stderr.read() if err_messge: # 如果错误信息不为空,执行结果为错误信息 cmd_res = res.stderr.read() else: # 错误信息为空,执行结果为标准信息 cmd_res = res.stdout.read() except Exception as e: print(e) break conn.send(cmd_res) conn.close() tcp_server.close()
from socket import * import subprocess from socket import * ip_port = ("127.0.0.1",8000) back_log = 5 buffer_size = 1024 tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg = input("输入你要执行的命令:").strip() if not msg: continue if msg == "quit": break # 跳出循环 tcp_client.send(msg.encode('utf-8')) data = tcp_client.recv(buffer_size) print("客户端接收到的信息%s" % data.decode("gbk")) # 执行系统命令返回的字符编码是系统编码,所以要用系统编码解码 tcp_client.close()
七、粘包
1、什么是粘包现象
粘包:发送方发送两个字符串”hello”+”world”,接收方却一次性接收到了”helloworld”。
或者发送方发送两个字符串”hello”+”w”,接收方第一次接收到”hel”,第二次接收到“low”
2、为什么产生粘包

应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?
可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方从缓存中取数据不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
只有TCP有粘包现象,UDP永远不会粘包
如果发生了:
1.短间隔时间内,多次发送多个小字节数据包,因为TCP为了提高网络的利用率,会使用一个叫做Nagle的算法。该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送。如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端
2.发送方发送了一大数据包,服务端只收了一小部分,接收方下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包
情景一演示
from socket import * ip_port = ("127.0.0.1", 8000) back_log = 5 buffer_size = 1024 server = socket(AF_INET,SOCK_STREAM) server.bind(ip_port) server.listen(back_log) conn, addr = server.accept() data1 = conn.recv(buffer_size) print("第一次接收:",data1.decode("utf-8")) data2 = conn.recv(buffer_size) print("第二次接收:",data2.decode("utf-8")) data3 = conn.recv(buffer_size) print("第三次接收:",data3.decode("utf-8")) conn.close() server.close()
from socket import * ip_port = ("127.0.0.1", 8000) back_log = 5 buffer_size = 1024 tcp_client = socket(AF_INET, SOCK_STREAM) tcp_client.connect(ip_port) tcp_client.send("h".encode("utf-8")) tcp_client.send("w".encode("utf-8")) tcp_client.send("1".encode("utf-8")) tcp_client.close()
运行结果:
第一次接收: hw1
第二次接收:
第三次接收:
情景二演示
from socket import * ip_port = ("127.0.0.1", 8000) back_log = 5 buffer_size = 3 server = socket(AF_INET,SOCK_STREAM) server.bind(ip_port) server.listen(back_log) conn, addr = server.accept() data1 = conn.recv(buffer_size) print("第一次接收:",data1.decode("utf-8")) data2 = conn.recv(buffer_size) print("第二次接收:",data2.decode("utf-8")) data3 = conn.recv(buffer_size) print("第三次接收:",data3.decode("utf-8")) conn.close() server.close()
from socket import * ip_port = ("127.0.0.1", 8000) back_log = 5 buffer_size = 1024 tcp_client = socket(AF_INET, SOCK_STREAM) tcp_client.connect(ip_port) tcp_client.send("hello".encode("utf-8")) tcp_client.send("world".encode("utf-8")) tcp_client.send("12345".encode("utf-8")) tcp_client.close()
运行结果
第一次接收: hel
第二次接收: low
第三次接收: orl
3、怎么解决粘包
方法一、先将待发送的数据的长度告诉对方,然后while recv_size < length循环接收(不能直接将缓存区大小设置为接收到的长度,因为会出现待接收数据大于系统内存情况)
import struct import subprocess from socket import * ip_port = ("127.0.0.1", 8000) 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() while True: try: cmd = conn.recv(buffer_size).decode("utf-8") if not cmd: break print("接收到命令", cmd) res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) err_messge = res.stderr.read() if err_messge: # 如果错误信息不为空,执行结果为错误信息 cmd_res = err_messge else: # 错误信息为空,执行结果为标准信息 cmd_res = res.stdout.read() if not cmd_res: # 当执行没有返回值的命令时,直接返回执行成功 cmd_res = "执行成功".encode("gbk") # 解决粘包 length = len(cmd_res) # 计算待发送数据长度 data_length = struct.pack('i', length) # 将int类型转换为固定4byte长度的字节流 conn.send(data_length) # 发送待发送数据长度 conn.send(cmd_res) # 发送真实需要发送的数据 except Exception as e: print(e) break
import struct from socket import * ip_port = ("127.0.0.1", 8000) back_log = 5 buffer_size = 1024 tcp_client = socket(AF_INET, SOCK_STREAM) tcp_client.connect(ip_port) while True: msg = input("输入你要执行的命令:").strip() # if not msg: # continue if msg == "quit": break # 跳出循环 tcp_client.send(msg.encode('utf-8')) #解决粘包 length_data = tcp_client.recv(4) # 接收待接收信数据的长度信息 length = struct.unpack("i",length_data)[0] # 解析length_data print("待接收数据长度:",length) # 接收真实的数据 recv_size = 0 recv_data = b'' while recv_size < length: recv_data += tcp_client.recv(buffer_size) # 接收数据 recv_size = len(recv_data) # 不能加1024,如果加进度条,会计算有误 # data = tcp_client.recv(buffer_size) print("客户端接收到的信息:%s" % recv_data.decode("gbk")) tcp_client.close()
使用到的struct模块参考
https://www.cnblogs.com/sexyboy/p/8509666.html
方法二、
八、使用socketserver实现多线程
1、一个简单的实例

import socketserver class Mysocket(socketserver.BaseRequestHandler): def handle(self): print("conn:", self.request) # 相当于socket中conn (conn, addr = tcp_server.accept()) print("addr", self.client_address) # 想相当于socket中addr while True: try: data = self.request.recv(1024) # 收消息 if not data: break print("收到消息是:%s,来自%s" %(data,self.client_address)) self.request.sendall(data.upper()) except Exception as e: print(e) break ip_port = ("127.0.0.1",8000) if __name__ == '__main__': s = socketserver.ThreadingTCPServer(ip_port,Mysocket) # 多线程 s.serve_forever()
多线程服务端

from socket import * import struct from functools import partial ip_port=('127.0.0.1',8000) 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')) msg = tcp_client.recv(buffer_size) print("接收到服务端返回信息:",msg.decode("utf-8")) tcp_client.close()
客户端
2、基于udp的多线程
import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): print(self.request) # udp中request是一个元组,第一个元素时接收的数据,第二个是套接字对象 data = self.request[0] print("接收到的数据是:",data.decode("utf-8")) self.request[1].sendto(data.upper(),self.client_address) ip_port = ("127.0.0.1",8001) if __name__ == "__main__": s = socketserver.ThreadingUDPServer(ip_port,Myserver) s.serve_forever()
from socket import * ip_port = ("127.0.0.1", 8001) buffer_size = 1024 udp_client = socket(AF_INET, SOCK_DGRAM) # 创建一个udp协议的套接字 # 循环发送接收数据 while True: msg = input("请输入发送内容:").strip() udp_client.sendto(msg.encode("utf-8"), ip_port) # 发送给服务端,需要两个参数:1、发送的数据 2、服务器地址 data, addr = udp_client.recvfrom(buffer_size) # 接收服务端发来的数据 print("接收到服务端发来的数据:", data)
浙公网安备 33010602011771号