Python之socket网络编程

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()
客户端
原文地址:https://www.cnblogs.com/zhang-can/p/7197460.html