网络基础,socket,黏包,socketserver

一  网络基础

1. 因此ip地址精确到具体的一台电脑,而端口精确到具体的程序。

2. iosi七层模型

互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层

 每层常见物理设备

每层常见协议

 

二  socekt

1. tcp/udp协议

TCP(Transmission Control Protocol)可靠的、面向连接的协议、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

  现在Internet上流行的协议是TCP/IP协议,该协议中对低于1024的端口都有确切的定义,他们对应着Internet上一些常见的服务。这些常见的服务可以分为使用TCP端口(面向连接)和使用UDP端口(面向无连接)两种。端口可用的为1025-65535

2. 基于tcp协议的socket

2.1 server端

import socket
sk = socket.socket()  # 实例化
sk.bind(('localhost', 8898))  # 把地址绑定到套接字,元组类型,回环地址
sk.listen()  # 监听链接
conn, addr = sk.accept()  # 接受客户端链接,accept()返回为: return sock, addr
ret = conn.recv(1024)  # 接收客户端信息
print(ret)  # 打印客户端信息
conn.send(b'hi')  # 向客户端发送信息,必须是bytes类型
conn.close()  # 关闭客户端套接字
sk.close()  # 关闭服务器套接字

2.2 client端

import socket
sk = socket.socket()  # 创建客户套接字
sk.connect(('localhost', 8898))  # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024)  # 对话(发送/接收)
print(ret)
sk.close()  # 关闭客户套接字

2.3 服务端重启问题解决

  服务器端重启的时候若报错类型为:OSError

# 加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET, SO_REUSEADDR   #允许端口重用,开启多个server端,实际中少用
sk = socket.socket()
sk.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  #此句,在bind前加
sk.bind(('localhost', 8898))  # 把地址绑定到套接字
sk.listen()  # 监听链接
conn, addr = sk.accept()  # 接受客户端链接
ret = conn.recv(1024)  # 接收客户端信息
print(ret)  # 打印客户端信息
conn.send(b'hi')  # 向客户端发送信息
conn.close()  # 关闭客户端套接字
sk.close()  # 关闭服务器套接字(可选)

2.4 socket模块方法

import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。

获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()            发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall()         发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom()        接收UDP数据
s.sendto()          发送UDP数据
s.getpeername()     连接到当前套接字的远端的地址
s.getsockname()     当前套接字的地址
s.getsockopt()      返回指定套接字的参数
s.setsockopt()      设置指定套接字的参数
s.close()           关闭套接字

面向锁的套接字方法
s.setblocking()     设置套接字的阻塞与非阻塞模式
s.settimeout()      设置阻塞套接字操作的超时时间
s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno()          套接字的文件描述符
s.makefile()        创建一个与该套接字相关的文件

3. 基于UDP协议的socket

3.1 server端 

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)  # 创建一个服务器的套接字
udp_sk.bind(('127.0.0.1', 9000))  # 绑定服务器套接字
msg, addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi', addr)  # 对话(接收与发送)
udp_sk.close()  # 关闭服务器套接字

3.2 client端   

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

4. subprocess模块

4.1 参数说明:

  该模块能够创建一个新的进程让其执行另外的程序,并与它进行通信,获取标准的输入、标准输出、标准错误以及返回码等。 主要应用该模块中的Popen类实现远程命令。

#Popen的初始化函数及参数说明:
def __init__(self, args, bufsize=-1, executable=None,
stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=True,
shell=False, cwd=None, env=None, universal_newlines=None,
startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False,
pass_fds=(), *, encoding=None, errors=None, text=None):

args:args should be a string, or a sequence of program arguments.用于指定进程的可执行文件及其参数。如果是一个序列类型参数,则序列的第一个元素通常都必须是一个可执行文件的路径。当然也可以使用executeable参数来指定可执行文件的路径。

stdin,stdout,stderr:分别表示程序的标准输入、标准输出、标准错误。有效的值可以是PIPE,存在的文件描述符,存在的文件对象或None,如果为None需从父进程继承过来,stdout可以是PIPE,表示对子进程创建一个管道,stderr可以是STDOUT,表示标准错误数据应该从应用程序中捕获并作为标准输出流stdout的文件句柄。

shell:如果这个参数被设置为True,程序将通过shell来执行。 

env:它描述的是子进程的环境变量。如果为None,子进程的环境变量将从父进程继承而来。

4.2 实例化

res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

cmd:标准像子进程传入需要执行的shell命令,如:ls -al

subprocess.PIPE:在创建Popen对象时,subprocess.PIPE可以初始化为stdin, stdout或stderr的参数,表示与子进程通信的标准输入流,标准输出流以及标准错误。

subprocess.STDOUT:作为Popen对象的stderr的参数,表示将标准错误通过标准输出流输出。

4.3 方法和属性

1、Popen.pid()

  获取子进程的进程ID。

2、Popen.returncode ()

  获取进程的返回码。如果进程未结束,将返回None。

3、communicate(input=None) 

  与子进程进行交互,像stdin发送数据,并从stdout和stderr读出数据存在一个tuple中并返回。参数input应该是一个发送给子进程的字符串,如果未指定数据,将传入None。

4、poll() 

  检查子进程是否结束,并返回returncode属性。

5、wait()

  等待子进程执行结束,并返回returncode属性,如果为0表示执行成功。

6、send_signal( sig)

  发送信号给子进程。

7、terminate()

  终止子进程。windows下将调用Windows API TerminateProcess()来结束子进程。

8、kill() 

  官方文档对这个函数的解释跟terminate()是一样的,表示杀死子进程。

4.4远程命令实例

  os.popen()缺点,发生错误,不能讲错误信息返回,所以是有subprocess.Popen()方法。

# server端
import socket
import subprocess
sk = socket.socket()
sk.bind(('localhost', 5456))
sk.listen(5)
while 1:
    conn, addr = sk.accept()
    while 1:
        cmds = conn.recv(1024)
        if cmds.decode() == 'q':break
        res = subprocess.Popen(cmds.decode(), shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
        if res.stdout:
            conn.send(res.stdout.read())  # read()返回的是GBK的bytes类型
        else:
            conn.send(res.stderr.read())  # 未报错返回None,Noen没有read()方法,直接运行报错
    conn.close()

   客户端为:

#client端
import socket
sk = socket.socket()
sk.connect(('localhost', 5456))
while 1:
    cmds = input('>>>>')           #输入的数据不能输字符串,出错
    if not cmds:
        continue
    sk.send(cmds.encode())
    data = sk.recv(102400)
    print(data.decode('gbk'))

三  黏包

1. 黏包成因

  • 发送端:TCP协议采用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据合并,然后进行封包一起发送,接收端就难于分辨。
  • 接收端:接受端接受数据过小或接受不及时,导致发送的数据在接收端的缓存区内堆积,造成黏包。

  UDP不采用合并优化算法,接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),对于接收端来说,就容易进行区分处理了。对于空消息:tcp是基于数据流的,收发的消息为空会使程序卡住;而udp是基于数据报的,输入的空内容udp会封装上消息头发送过去。 总结如下:

  • 黏包只会在tcp中产生(发送端和接收端两种原因),udp中不会
  • tcp不能发送空消息,而udp可以发送空消息

拓展:当发送端缓冲区的长度大于网卡的MTU时,tcp、udp会将这次发送的数据拆成几个数据包发送出去。 MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。UDP包大小有上限,即64K。

2. stuct 解决黏包

2.1 struct模块

  该模块可以把一个类型,如数字,转成固定长度的bytes

import struct
ret = struct.pack('i', 183346)  # 将一个数字转化成等长度的bytes类型。
print(ret, type(ret), len(ret))

ret1 = struct.unpack('i', ret)[0]  # 通过unpack反解回来,整体以元组返回(183346,)
print(ret1, type(ret1))

#ret = struct.pack('l', 4323241232132324)  # 但是通过struct 处理不能处理太大
#print(ret, type(ret), len(ret))  # 报错

b'2xccx02x00' <class 'bytes'> 4;183346 <class 'int'>

   具体参数含义如下:

 

 2.2 定制报头形式

  网络上传输的所有数据都叫数据包,里面的数据叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,所有的报文都有报头,这个报头是协议规定的。我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。

发送时:

  • 先发报头长度
  • 再编码报头内容
  • 然后发送最后发真实内容

接收时:

  • 先接受报头长度,用struct取出来
  • 根据取出的长度收取报头内容,然后解码,反序列化
  • 从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
# server端
import socket, json, struct
import subprocess
sk = socket.socket()
sk.bind(('localhost', 5456))
sk.listen(5)
while 1:
    conn, addr = sk.accept()
    while 1:
        cmds = conn.recv(1024)
        if cmds.decode() == 'q': break
        res = subprocess.Popen(cmds.decode(), shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
        ret = res.stdout.read() if res.stdout else res.stderr.read()
        header_dict = {'data_size': len(ret)}
        header_bytes = json.dumps(header_dict).encode()
        
        conn.send(struct.pack('i',len(header_bytes)))    #发送4位报头大小
        conn.send(header_bytes)      #发送报头  
        conn.send(ret)        #发送数据
    conn.close()

 客户端:

#client端
import socket,json,struct
sk = socket.socket()
sk.connect(('localhost', 5456))
while 1:
    cmds = input('>>>>')
    if not cmds:continue
    sk.send(cmds.encode())

    header_size = sk.recv(4)  
    header_size = struct.unpack('i',header_size)[0]  #报头长度
    head_dict = json.loads(sk.recv(header_size).decode())  #报头
    data_size = head_dict['data_size']

    recv_size = 0
    recv_data = b''
    while recv_size < data_size:
        recv_data += sk.recv(1024)
        recv_size += len(recv_data)
    print(recv_data.decode('gbk'))

四  socketserver

  主要解决TCP中server不能同时连接多个client的问题,只修改server,不用修改client

import socketserver
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):  # handle名称固定,逻辑代码写在该类下
        self.data = self.request.recv(1024).decode() #self.request = conn
        print(self.data)
        self.request.sendall(self.data.upper().encode())
server = socketserver.TCPServer(('localhost', 5456), Myserver)
server.serve_forever()  # 开启永久服务

 

原文地址:https://www.cnblogs.com/mushuiyishan/p/10475913.html