1.什么是socket
了解socket之前首先要了解osi七层模型,或者说五层模型:应用层,传输层,网络层,数据链路层,物理层
其中,数据一定是转化成物理信号通过物理层的物理设备才能传输;但是我们不谈物理设备,我们今天的重点是 :网络上的两个程序怎么通过一个连接实现数据交换。怎么唯一标示一个程序呢,是通过ip+端口的方式。socket就是用来描述ip和端口的,可以理解socket为把tcp,udp协议封装成一组接口,我们不需要知道复杂的tcp/udp协议,只需要按照socket的规定编程,写出的程序自然就是遵循tcp/udp协议的。
socket又被称为套接字,我们研究套接字只需要记住两件事:建立连接,收发数据,就行了在写代码之前我们还需要知道一个c/s模型,就是client/sever(客户端与服务端),交换数据至少得两个人吧,在网络编程中就是客户端和服务端。客户端向服务端发送请求,服务端响应客户端的请求。
我们先写一个简单的基于tcp的socket程序:
#服务端 import socket phone_sever=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone_sever.bind(('127.0.0.1',8080))#绑定ip和端口,我们用的本地回环网卡演示 phone_sever.listen(5)#监听,等待连接 conn,addr=phone_sever.accept() #客户端连入 ret=conn.recv(1024)#接收客户端发来的消息 conn.close()#断开连接 phone_sever.close()#服务端关闭
#客户端 import socket phone_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone_client.connect(('127.0.0.1',8080))#连接服务端的ip和端口 phone_client.send('hello'.encode('utf-8'))#向服务端发送消息 phone_client.close()#客户端关闭
写好后,先运行服务端,在运行客户端,这样就成功写了一个socket。这是最简单的socket例子了,只是介绍了基本语法,我们下面优化一下代码,
注意:传输的数据都应该是bytes类型
2.基于TCP的套接字
我们基于上面的例子,优化一下代码
1.服务端应该是循环接收数据,并能循环发送数据
2.客户端应该也能接收服务端的消息,并且也是循环收发
3.服务端可以与多个客户端连接
4.某一个客户端断开连接或者发生故障不能导致服务端崩溃
5.顺便模拟一下客户端输入命令,服务端把命令的执行结果返回给客户端
6.解决OSError:[Errno 48] Address already in use的问题
#服务端 import subprocess import socket phone_sever=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机 phone_sever.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#解决问题6,加上这句话就行 phone_sever.bind(('127.0.0.1',8080))#这里是元组形式 phone_sever.listen(5) print('sever run ') while True:#这个循环解决问题3 conn,client_addr=phone_sever.accept() print('客户端',client_addr) while True:#这个while循环解决问题1 try:#捕捉异常,解决问题4 cmd=conn.recv(1024)#收消息 res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)#利用subprocess这个模块,解决问题5 stdout=res.stdout.read() stderr=res.stderr.read() sever_res=stdout+stderr if not sever_res: sever_res=b'is vaild' conn.sendall(sever_res)#发消息 except Exception: break conn.close()#挂电话 phone_sever.close()#关机
#客户端
import socket phone_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机 phone_client.connect(('127.0.0.1',8080))#拨号 # phone_client.connect(('192.168.16.253',8080))#拨号 while True:#这个循环解决问题2 msg=input('>>>>:') if not msg:continue#解决客户端输入空,服务端会死掉的问题 phone_client.send(msg.encode('utf-8')) server_res=phone_client.recv(1024) print('server_res:',server_res.decode('gbk')) phone_client.close()
当我们在实验的时候,可能会遇到这种情况:客户端输入为空,服务端就卡住了。解释这个问题就要说到网络传输的原理了
我们前面说了,两台机器之间传输数据一定是通过底层网卡等物理设备的,而应用程序是不能操作底层硬件的,这时就需要调用操作系统了,当用send发数据时,其实是由应用程序把数据发给操作系统,然后由操作系统把数据放到一块缓存上,然后由这块缓存再将数据发送给服务端的缓存,如果我们send的是一个空数据,那肯定就不会发送成功了,但是我们的客户端以为按了回车就发送了,就开始等待服务端的回应了,所以就卡住了。
3.基于UDP的套接字
#服务端
import socket udp_sever=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_sever.bind(('127.0.0.1',8080)) while True: msg,addr=udp_sever.recvfrom(1024) print(msg,addr) udp_sever.sendto(msg.upper(),addr)
#客户端 import socket ip_port=('127.0.0.1',8080) udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>>>>>:').strip() if not msg:continue udp_client.sendto(msg.encode('utf-8'),ip_port) back_msg,addr=udp_client.recvfrom(1024) print(back_msg.decode('utf-8'),addr)
udp不需要建立连接,所以可以实现与多个客户端同时建立连接。但是相比于tcp,udp是不可靠传输,但是速度快。可以把tcp协议理解为打电话,必须双方建立连接,然后说一句回一句。udp就像是发短信,客户端只关心消息有没有发出去就行了,不用关心对方有没有收到。具体udp的不可靠传输,下面会说到
在代码方面的区别,tcp的recv就相当于udp的recvfrom,tcp的send就相当于udp的sendto,另外因为udp不建立连接,所以发送消息的时候需要指定ip和端口号
4.粘包现象
先来看我们前面写的tcp的代码,recv()括号里的1024,这个1024限制了接收消息的最大字节数,想象一下,如果我们接收的数据长度超过1024字节,为了便于观察,我们把这个接受消息的最大字节数改成10,代码如下,看看如果发送的消息超过10,接收端会发生什么:
1 import subprocess 2 3 import socket 4 phone_sever=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机 5 phone_sever.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 6 phone_sever.bind(('127.0.0.1',8080)) 7 8 phone_sever.listen(5) 9 10 print('sever run ') 11 while True: 12 13 conn,client_addr=phone_sever.accept() 14 print('客户端',client_addr) 15 while True: 16 try: 17 cmd=conn.recv(10)#收消息 18 print(cmd.decode('utf-8')) 19 msg=input('>>>>>').strip() 20 conn.send(msg.encode('utf-8')) 21 except Exception: 22 break 23 conn.close()#挂电话 24 25 phone_sever.close()#关机
1 import socket 2 3 phone_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 phone_client.connect(('127.0.0.1',8080)) 5 while True: 6 msg=input('>>>>:') 7 if not msg:continue 8 phone_client.send(msg.encode('utf-8')) 9 10 server_res=phone_client.recv(10)#收消息 11 print('server_res:',server_res.decode('gbk')) 12 13 phone_client.close()
在发一条消息
是不是,问题就来了。如果发送的消息超过设置的最大字节数,在tcp中就会分批发送,这样肯定是有问题的,这样就没办法保证我们数据的完整性了。我们先来解释一下这个现象:
我们已经说过了,不管客户端还是服务端,他们都是处在应用层的应用程序,没有权利去操作硬件。所谓发送的数据都是要交给操作系统的,由操作系统调用网卡等硬件设备将消息转成物理信号发送出去。在这个过程中,操作系统需要把数据从用户态copy到内核态,才能调用网卡。这种频繁的copy,切换是很占系统资源的,为了解决这个问题,加了一个缓存的机制。等缓存中的数据满了或者超过了时间间隔,就会把数据发送出去。这个缓存默认是8K,也就是8192字节,所以我们最大设置的recv()括号里的数就是8192。还没说到点上,有点跑偏。假设缓存中有两个数据,a数据有100字节,b数据有50字节,用户设置的每次接收80字节,想想会发生什么,第一次收到了a数据的80字节,第二次呢,第二次就会收到a数据剩下的20字节,加上b数据的50字节。因为tcp是流式的,就像水桶里的水,不同的数据会黏在一起(当然并不是没有边界),这种现象就叫‘粘包’。这种现象在windows和linux中都有。
然而udp中是不存在粘包现象的,因为udp并没有建立连接,udp遇到这种消息超出设置字节数的情况的处理办法是:直接丢弃。这也体现了udp的不可靠
然后最重要的来了,怎么解决粘包问题。既然我们已经知道了粘包的原理,那就见招拆招呗,因为我们不知道对方发送的数据长度,所以才会出现这种问题,那就让对方发送数据的时候,把数据长度先发过来,然后在recv()括号里写上对方发送来的长度,但是有个问题,刚才说了缓存只有8K,所以这个括号里的数字最好不要超过8192,那就循环接收,一次收不完数据,就分几次接收,这是可行的。还有个问题,那个发送数据长度的消息,会不会发生粘包的问题呢,所以我们应该把这条消息写成是固定长度的。说到长数据,干脆发个视频文件吧,废话不多说,上代码:
1 #服务端 2 import socket 3 import os,json 4 5 #客户端请求服务端的文件,服务端确认后将文件发送给客户端 6 sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 7 sock.bind(("127.0.0.1",9999)) 8 sock.listen(1) 9 10 def pack_msg_header(header,header_size):#制作文件头部 11 bytes_header = bytes(json.dumps(header) ,encoding="utf-8") 12 13 if len(bytes_header ) < header_size :#需要补充0 14 header['fill'].zfill( header_size - len(bytes_header) ) 15 bytes_header = bytes(json.dumps(header), encoding="utf-8") 16 return bytes_header 17 18 while True: 19 20 conn,addr = sock.accept() #等待、阻塞 21 print("got a new customer",conn,addr) 22 23 while True: 24 raw_cmd = conn.recv(1024) # get test.log 25 cmd,filename = raw_cmd.decode("utf-8").split() 26 if cmd == "get": 27 msg_header = {"fill": ''} 28 if os.path.isfile(filename): 29 msg_header["size"] = os.path.getsize(filename) 30 msg_header["filename"] = filename 31 msg_header["ctime"] = os.stat(filename).st_ctime 32 bytes_header = pack_msg_header(msg_header,300)#规定头文件长度为300 33 34 conn.send(bytes_header) 35 36 f = open(filename,"rb") 37 for line in f: 38 conn.send(line) 39 40 else: 41 print("file send done....") 42 f.close() 43 else: 44 msg_header['error'] = "file %s on server does not exist " % filename 45 bytes_header = pack_msg_header(msg_header, 300) 46 conn.send(bytes_header) 47 48 49 sock.close()
1 #客户端 2 import socket 3 import json 4 5 sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 6 sock.connect(('12.0.0.7',9999)) 7 8 while True: 9 cmd = input(">>>:").strip() 10 if not cmd : continue 11 sock.send(cmd.encode("utf-8")) 12 msg_header = sock.recv(300)#规定头文件的长度为300 13 print("received:",msg_header.decode("gbk")) 14 15 header = json.loads(msg_header.decode("utf-8")) 16 if header.get("error"): 17 print(header.get("error")) 18 else: 19 filename = header['filename'] 20 file_size = header['size'] 21 f = open(filename,"wb") 22 received_size = 0 23 24 while received_size < file_size : 25 if file_size - received_size < 8192:#循环接收的最后一次 26 data = sock.recv(file_size - received_size) 27 else: 28 data = sock.recv(8192) 29 30 received_size += len(data) 31 f.write(data) 32 else: 33 print("file receive done....",filename,file_size) 34 f.close() 35 36 37 sock.close()