网络通信

zhangly 2021-11-06 23:16:35
Categories: > Tags:

互联网协议

按照功能的不同分为七层。

OSI七层协议:应,表,会,传,网,数,物。

其中应表会可以归为应用层

tcp也称作流式协议,需要双向管道(来回)。

通过握手确认建立临时管道。

udp协议不需要通道,不会等待对方确认是否收到。

效率比tcp协议高,但是不可靠。(只负责丢)

socket层

在应用层和传输层(tcp/udp)之间的中间软件抽象层,它是一组接口。

socket为套接字编程。

一个简单的服务端和客户端

服务端

iimport socket

# 创建一个套间字对象。 (socket.AF_INET 套间字类型    socket.SOCK_STREAM 是tcp传输协议)
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定一个端口。 port: 0-65535 其中0-1024是给操作系统使用
phone.bind(('127.0.0.1', 8080))

# 开始监听。 参数为最大挂起的连接数
phone.listen(5)

# 服务端会在这里进行等待连接接入,直到有连接才会进行下一步
conn, client_address = phone.accept()

# 接收消息,限制为:1024 bytes
data = conn.recv(1024)
print(data)

# 回复信息给客户端
conn.send(data.upper())

# 关闭连接和关闭服务
conn.close()
phone.close()

客户端

iimport socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.connect(('127.0.0.1', 8080))

# 发送数据到服务端时,只能发送二进制编码字符(这里用encode将字符串转换为二进制类型)
phone.send('hello'.encode('utf-8'))

# 从服务端接受回信息
data = phone.recv(1024)
print(data)
phone.close()

改进客户端和服务端,实现循环通信

服务端

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('staring...')

while True:  # 连接循环
    conn, client_address = phone.accept()

    while True:  # 通信循环
        try:
            data = conn.recv(1024)
            # 适用于linux系统,客户端断开连接
            if not data:
                break
            print(data)
            conn.send(data.upper())
        except ConnectionResetError:  # 适用于windows系统,客户端断开连接
            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'))
    data = phone.recv(1024)
    print(data.decode('utf-8'))

phone.close()

实现类似ssh的效果(远程执行命令)

服务端

import socket
import subprocess

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('staring...')

while True:  # 连接循环
    conn, client_address = phone.accept()

    while True:  # 通信循环
        try:
            cmd = conn.recv(1024)
            # 适用于linux系统,客户端断开连接
            if not cmd:
                break
            # 执行命令
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stdout.read()
            conn.send(stdout+stderr)

        except ConnectionResetError:  # 适用于windows系统,客户端断开连接
            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:
    cmd = input('>> ').strip()
    if not cmd:
        continue
    phone.send(cmd.encode('utf-8'))
    data = phone.recv(1024)
    print(data.decode('utf-8'))

phone.close()

这里会留下一个问题,每次接受限制了1024个字节,会产生了粘包现象。

粘包现象

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

底层原理

1.不管是recv还是send都不是直接接受对方的消息,而是操作自己的操作系统。

(所以recv没法超过自己内存的大小)

2.tcp协议会对间隔短的多个包进行优化合并

3.recv和send方法,可以一直调用,直到内存里的值被取完

解决粘包现象

一种方法:先把数据的长度发送给客户端,再发送真实数据。

服务端

import socket
import struct
import subprocess

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('staring...')

while True:  # 连接循环
    conn, client_address = phone.accept()

    while True:  # 通信循环
        try:
            cmd = conn.recv(1024)
            if not cmd:
                break
            # 执行命令
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stdout.read()

            # 计算结果的总字节数
            total_size = len(stdout) + len(stderr)
            # 使用struct打包结果,返回的值永远为4字节
            header = struct.pack('i', total_size)

            # 先发送报头,即数量的字节大小
            conn.send(header)

            # 再发送数据
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break
    conn.close()

phone.close()

客户端

import socket
import struct

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'))

    # 先接收数据大小,永远返回4个字节
    header = phone.recv(4)
    total_size = struct.unpack('i', header)[0]

    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = phone.recv(1024)
        recv_data += res
        recv_size += len(res)

    print(recv_data.decode('utf-8'))

phone.close()

上述的方法有个弊端是,pack方法打包不能超过2147483647

# 可以使用长整数类型,不过也是有限制的
res = struct.pack('l', 10000000000000)
print(res, len(res)) # 返回8字节

另一个解决办法:

1.先发送报头长度

2.再发送报头的数据

3.最后发送真实数据

服务端

import json
import socket
import struct
import subprocess

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('staring...')

while True:  # 连接循环
    conn, client_address = phone.accept()

    while True:  # 通信循环
        try:
            cmd = conn.recv(1024)
            if not cmd:
                break
            # 执行命令
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stdout.read()

            # 把所有结果打包成字典
            header_dic = {
                'filename': 'test.txt',
                'size': len(stdout) + len(stderr)
            }
            # 再将字典序列化,然后打包为报头
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('utf-8')

            # 先发送报头长度
            conn.send(struct.pack('i', len(header_bytes)))

            # 再发送报头
            conn.send(header_bytes)

            # 最后发送数据
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break
    conn.close()

phone.close()

客户端

import json
import socket
import struct

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'))

    # 先接收报头长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]

    # 再接收报头
    header_bytes = phone.recv(header_size)

    # 解析需要的真实数据
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic['size']

    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = phone.recv(1024)
        recv_data += res
        recv_size += len(res)

    print(recv_data.decode('utf-8'))

phone.close()

文件传输功能

服务端

import json
import os
import socket
import struct
import subprocess

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('staring...')

while True:  # 连接循环
    conn, client_address = phone.accept()

    while True:  # 通信循环
        try:
            res = conn.recv(1024)  # 比如这里使用的是 get a.txt 命令
            if not res:
                break

            cmds = res.decode('utf-8').split()
            filename = cmds[1]

            # 把所需数据写成字典
            header_dic = {
                'filename': filename,
                'size': os.path.getsize(filename)
            }

            # 再将字典序列化,然后打包为报头
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('utf-8')

            # 先发送报头长度
            conn.send(struct.pack('i', len(header_bytes)))

            # 再发送报头
            conn.send(header_bytes)

            # 最后发送文件数据
            with open(filename, 'rb') as f:
                for line in f:
                    conn.send(line)

        except ConnectionResetError:
            break
    conn.close()

phone.close()

客户端

import json
import socket
import struct

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'))

    # 先接收报头长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]

    # 再接收报头
    header_bytes = phone.recv(header_size)

    # 解析需要的真实数据
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic['size']
    filename = header_dic['filename']

    # 写入数据
    with open(filename + '_download', 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)
            f.write(line)
            print('{0}/{1}'.format(recv_size, total_size))
            recv_size += len(line)

phone.close()

udp协议实现

比如查询操作就适合udp协议,效率快,不需要双方进行握手确认,且不会产生粘包现象。

(虽然不会粘包,但是数据超出限制则会丢失)

服务端

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))

while True:
    data, client_addr = server.recvfrom(1024)
    server.sendto(data.upper(), client_addr)

客户端

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
    msg = input('>>: ').strip()
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))

    data, server_addr = client.recvfrom(1024)
    print(data, server_addr)