1.了解socket之前先回顾一下osi网络七层协议
每层常见的物理设备有:
2.tcp建立链接的过程,三次握手,四次挥手。
具体每一层的具体作用,参考链接:http://www.cnblogs.com/linhaifeng/articles/5937962.html
进入正题
3.什么是socket
socket是应用层与tcp/ip协议簇通信的中间软件抽象层,他是一组接口。
4.套接字
套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我们只使用AF_INET)
5.套接字的工作流程:
可以借助一个生活中的例子去理解套接字的工作流程。
你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。
6.socket()模块函数用法
1 import socket 2 socket.socket(socket_family,socket_type,protocal=0) 3 socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。 4 5 获取tcp/ip套接字 6 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 7 8 获取udp/ip套接字 9 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 10 11 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。 12 例如tcpSock = socket(AF_INET, SOCK_STREAM)
7。基于tcp的套接字
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
(1).基本版的通信:
服务端:
import socket #买手机 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #插卡 phone.bind(('127.0.0.1',8080)) #开机 phone.listen(5) #等电话链接 print('server start...') conn,client_addr=phone.accept() #(tcp链接,client_addr) print('链接',conn) print(client_addr) #基于建立的链接,收发消息 client_data=conn.recv(1024) print('客户端的消息',client_data) conn.send(client_data.upper()) #挂电话链接 conn.close() #关机 phone.close()
客户端:
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) phone.send('hello'.encode('utf-8')) server_data=phone.recv(1024) print('服务端回应的消息',server_data) phone.close()
上述代码就是一个基本的模拟客户端跟服务端通信过程。
但是存在很多问题,比如,上述代码客户端建立连接发送一条消息就终止了,这显然没有什么意义。并且只能同一个客户端连接,连接断开了就终止了,其他客户端没办法再次建立连接了。
所以我们要加上连接循环跟通信循环解决上述两个问题:
(2)加上连接循环跟通讯循环:
服务端:
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #解决地址占用的问题 phone.bind(('127.0.0.1',8080)) phone.listen(5) print('server start...') while True: #链接循环 conn,client_addr=phone.accept() print(conn,client_addr) while True: #通讯循环 try: client_data=conn.recv(1024) if not client_data:break #针对linux系统 # print('has rev') conn.send(client_data.upper()) except Exception: #针对windwos break conn.close() phone.close()
客户端:
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if not msg:continue phone.send(msg.encode('utf-8')) # print('====>has send') server_data=phone.recv(1024) # print('====>has recv') print(server_data.decode('utf-8')) phone.close()
8.tcp的粘包现象:
只有TCP有粘包现象,UDP永远不会粘包
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
为什么粘包只有TCP有?
下面来看一下两个协议的差别
(1).TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
(2)UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
(3)tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
TCP在什么情况下会产生粘包呢?
(1)发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
(2)接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
9.解决粘包现象的方法。
思路:为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
代码:(下面是模拟了一个连接到服务器ssh远程执行命令的情形,来看怎么解决粘包现象)
服务端:
import socket import struct #这个模块的作用是可以把一个类型,如数字,转成固定长度的bytes import subprocess import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) phone.listen(5) print('server start...') while True: #链接循环 conn,client_addr=phone.accept() print(conn,client_addr) while True: #通讯循环 try: cmd=conn.recv(1024) if not cmd:break #执行命令,拿到结果 res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=res.stdout.read() stderr=res.stderr.read() #制作报头 header_dic={'total_size':len(stdout)+len(stderr),'md5':None} header_json=json.dumps(header_dic) header_bytes=header_json.encode('utf-8') #1 先发报头的长度(固定4个bytes) conn.send(struct.pack('i',len(header_bytes))) #2 先发报头 conn.send(header_bytes) #3 再发真实的数据 conn.send(stdout) conn.send(stderr) except Exception: #针对windwos break conn.close() phone.close()
客户端:
import socket import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while True: cmd=input('>>: ').strip() if not cmd:continue #发命令 phone.send(cmd.encode('utf-8')) #先收报头的长度 struct_res=phone.recv(4) header_size=struct.unpack('i',struct_res)[0] #再收报头 header_bytes=phone.recv(header_size) head_json=header_bytes.decode('utf-8') head_dic=json.loads(head_json) total_size=head_dic['total_size'] #再收命令的执行结果 recv_size=0 data=b'' while recv_size < total_size: recv_data=phone.recv(1024) recv_size+=len(recv_data) data+=recv_data #打印结果 print(data.decode('gbk')) phone.close()