网络编程之socket

网络编程之socket

C/S架构

client客户端
server服务端
B/S架构
b broser 浏览器
s server 服务端

TCP/TP 五层模型(实际上用TCP/TP四层模型,物理层与链路层合并)

  1. 应用层
  2. 传输层 tcp协议和udp协议
  3. 网络层 ip协议(ipv4 ipv6) 路由器
  4. 数据链路层 arp协议(利用ip找mac) 交换机
  5. 物理层

SOCKET

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。
封装好了,调参调用就行。
TCP协议属于 : 传输层,面向连接 可靠的 字节流传输 长连接
UDP协议属于 : 传输层,面向数据包的 无连接的 不可靠的 速度快 不占用连接
NAT技术: 将你的IP地址,转换为网关的IP地址
arp协议:通过IP地址找到mac地址

# TCP
# client
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 8080))
sk.send(b'hi ')
ret = sk.recv(1024)
print(ret)
sk.close()

# sercer
import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()
ret = conn.recv(1024)
print(ret)
conn.send(b'hi world')
conn.close()
sk.close()
# UDP
# client
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8888)
while True:
    msg = input('>>>').strip().encode('utf-8')
    if msg.decode('utf-8') == 'q':
        break
    client.sendto(msg, ip_port)
client.close()

# server
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # DGRAM : datagram  数据报文
ip_port = ('127.0.0.1', 8888)
server.bind(ip_port)
while True:
    data, addr = server.recvfrom(1024)
    print(data.decode('utf-8'))

tcp长连接,tcp属于长连接,长连接就是一直占用着这个链接,这个连接的端口被占用了,第二个客户端过来连接的时候,他是可以连接的,但是处于一个占线的状态,就只能等着去跟服务端建立连接,除非一个客户端断开了(优雅的断开可以,如果是强制断开就会报错,因为服务端的程序还在第一个循环里面),然后就可以进行和服务端的通信了。

# TCP长连接
# client
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 8090))  # 连接服务端
while True:
    msg = input('客户端>>>')  # input阻塞,等待输入内容
    sk.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
    ret = sk.recv(1024)
    ret = ret.decode('utf-8')
    print(ret)
    if ret == 'bye':
        break
sk.close()

# server
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
sk = socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #允许地址重用,能解决问题,不建议这么做,容易出问题
sk.bind(('127.0.0.1', 8090))
sk.listen()
# 第二步演示,再加一层while循环
while True:  # 下面的代码全部缩进进去,也就是循环建立连接,但是不管怎么聊,只能和一个聊,也就是另外一个优雅的断了之后才能和另外一个聊
    # 它不能同时和好多人聊,还是长连接的原因,一直占用着这个端口的连接,udp是可以的
    conn, addr = sk.accept()  # 在这阻塞,等待客户端过来连接
    while True:
        ret = conn.recv(1024)  # 接收消息,在这还是要阻塞,等待收消息
        ret = ret.decode('utf-8')  # 字节类型转换为字符串中文
        print(ret)
        if ret == 'bye':  # 如果接到的消息为bye,退出
            break
        msg = input('服务端>>')  # 服务端发消息
        conn.send(msg.encode('utf-8'))
        if msg == 'bye':
            break
    conn.close()
# 优雅的断开一个client端之后另一个client端就可以通信的代码
# UDP 多人聊天
# client
import socket
ip_port = ('127.0.0.1', 9000)
sk = socket.socket(type=socket.SOCK_DGRAM)
while True:
    msg = input('>>>')
    sk.sendto(bytes(msg, encoding='utf-8'), ip_port)
    msg_recv, addr = sk.recvfrom(1024)
    if msg_recv.decode('utf-8') == 'q':
        break
    print(msg_recv.decode('utf-8'), addr)
sk.close()

# server
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1', 9000))
while True:
    msg_recv, addr = sk.recvfrom(1024)
    if msg_recv.decode('utf-8') == 'q':
        sk.sendto(b'q', addr)
    print(msg_recv.decode('utf-8'), addr)
    msg = input('>>>')
    sk.sendto(msg.encode('utf-8'), addr)
sk.close()

粘包

出现原因:

接收方不知道消息之间的界限,不知道一次提取多少字节的数据所造成的。

缓冲区

什么是缓冲区,为什么会有缓冲区?
暂时存放数据,防止程序卡主(阻塞,网络延时),最大程度提高代码的运行效率。
输入缓冲区 #recv
输出缓冲区 #send
设置缓冲区 # getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);不过不管用,需要改操作系统配置文件

只有tcp有粘包,udp没有

tcp是面向连接,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
是因为udp是面向报文的,意思就是每个消息是一个包,你接收端设置接收大小的时候,必须要比你发的这个包要大,不然一次接收不了就会报这个错误,丢包。

补充问题一:为何tcp是可靠传输,udp是不可靠传输

tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的。
而udp发送数据,对端是不会返回确认信息的,因此不可靠

粘包(tcp的两种粘包现象)

  1. 连续发送小的数据,并且每次发送之间的时间间隔很短(输出缓冲区:两个消息在缓冲区黏在一起了)原因是TCP为了传输效率,做了一个优化算法(Nagle),减少连续的小包发送(因为每个消息被包裹以后,都会有两个过程:组包,拆包)
  2. 第一次服务端发送的数据比我客户端设置的一次接收消息的大小的小,导致部分数据留在了缓冲区。

出现原因:
接收方不知道消息之间的界限,不知道一次提取多少字节的数据所造成的。
解决方案

  1. 发送数据之前先发送数据长度,让接收端循环接受
    • 简易版
    • 升级版:发送固定长度的 数据长度
    • 自定义报文,先发送报文长度,发送报文,发送数据。
  2. 将发送的每条消息的首尾都加上特殊标记符
# 简易版

# client

import socket
client = socket.socket()
ip_port = ('127.0.0.1', 8899)
client.connect(ip_port)
while True:
    cmd = input('>>>')
    client.send(cmd.encode('utf-8'))
    if cmd == 'exit':
        break
    info_len = int(client.recv(1024).decode('utf-8'))
    print(' {} 命令返回的数据大小为 {} '.format(cmd, info_len))
    client.send('ok'.encode('utf-8'))
    ret_cmd = client.recv(info_len)
    print(ret_cmd.decode('gbk'))

# server

import socket
import subprocess
def execute_cmd(cmd):
    """
    在操作系统上执行命令并返回结果
    :param cmd: str
    :return: bytes gbk
    """
    ret = subprocess.Popen(
        cmd,    # 要执行的命令
        shell=True, # 表示要执行的是一条系统命令
        stdout=subprocess.PIPE, # 存储执行结果的正常信息
        stderr=subprocess.PIPE  # 存储执行结果的错误信息
    )
    stdout = ret.stdout.read()
    stderr = ret.stderr.read()
    return stdout if stdout else stderr
server = socket.socket()
ip_port = ('127.0.0.1', 8899)
server.bind(ip_port)
server.listen(5)
while True:
    conn, addr = server.accept()
    while True:
        ret = conn.recv(1024).decode('utf-8')
        if ret == 'exit':
            conn.close()
            break
        ret_cmd = execute_cmd(ret)
        ret_cmd_len = str(len(ret_cmd))
        print(' {} 命令返回数据的大小为 {} '.format(ret, ret_cmd_len))
        conn.send(ret_cmd_len.encode('utf-8'))
        flag = conn.recv(1024)
        if flag.decode('utf-8') == 'ok':
            conn.send(ret_cmd)
# 升级版:发送固定长度的数据长度  
# struct模块
# 该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
ret = struct.pack('i', 9000)
print(ret)
print(len(ret))
num = struct.unpack('i', ret)
print(num)
print(num[0])

# client
import socket
import struct
client = socket.socket()
ip_port = ('127.0.0.1', 8899)
client.connect(ip_port)
while True:
    cmd = input('>>>')
    client.send(cmd.encode('utf-8'))
    if cmd == 'exit':
        break
    ret_cmd_len_pack = client.recv(4)
    ret_cmd_len = struct.unpack('i', ret_cmd_len_pack)[0]
    all_ret = bytes()
    while ret_cmd_len > 0:
        if ret_cmd_len > 1024:
            all_ret += client.recv(1024)
            ret_cmd_len -= 1024
        else:
            all_ret += client.recv(ret_cmd_len)
            break
    print(all_ret.decode('gbk'))

# server
import socket
import subprocess
import struct
def execute_cmd(cmd):
    """
    在操作系统上执行命令并返回结果
    :param cmd: str
    :return: bytes gbk
    """
    ret = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout = ret.stdout.read()
    stderr = ret.stderr.read()
    return stdout if stdout else stderr
server = socket.socket()
ip_port = ('127.0.0.1', 8899)
server.bind(ip_port)
server.listen(5)
while True:
    conn, addr = server.accept()
    while True:
        ret = conn.recv(1024).decode('utf-8')
        if ret == 'exit':
            conn.close()
            break
        ret_cmd = execute_cmd(ret)
        ret_cmd_len = len(ret_cmd)
        ret_cmd_len_pack = struct.pack('i', ret_cmd_len)
        conn.send(ret_cmd_len_pack)
        conn.sendall(ret_cmd)
# 自定义报文
# client
import socket
import struct
import json
client = socket.socket()
ip_port = ('127.0.0.1', 8899)
client.connect(ip_port)
while True:
    cmd = input('>>>')
    client.send(cmd.encode('utf-8'))
    if cmd == 'exit':
        break
    headers_json_bytes_len_pack = client.recv(4)
    headers_json_bytes_len = struct.unpack('i', headers_json_bytes_len_pack)[0]
    headers_json_bytes = client.recv(headers_json_bytes_len)
    headers_json = headers_json_bytes.decode('utf-8')
    headers = json.loads(headers_json)
    ret_cmd_len = headers.get('data_size')
    all_ret = bytes()
    while ret_cmd_len > 0:
        if ret_cmd_len > 1024:
            all_ret += client.recv(1024)
            ret_cmd_len -= 1024
        else:
            all_ret += client.recv(ret_cmd_len)
            break
    print(all_ret.decode('gbk'))

# server
import socket
import subprocess
import struct
import json
def execute_cmd(cmd):
    """
    在操作系统上执行命令并返回结果
    :param cmd: str
    :return: bytes gbk
    """
    ret = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout = ret.stdout.read()
    stderr = ret.stderr.read()
    return stdout if stdout else stderr
# 报头{'data_size':int}
server = socket.socket()
ip_port = ('127.0.0.1', 8899)
server.bind(ip_port)
server.listen(5)
while True:
    conn, addr = server.accept()
    while True:
        ret = conn.recv(1024).decode('utf-8')
        if ret == 'exit':
            conn.close()
            break
        ret_cmd = execute_cmd(ret)
        ret_cmd_len = len(ret_cmd)
        headers = {'data_size': ret_cmd_len}
        headers_json = json.dumps(headers)
        headers_json_bytes = bytes(headers_json, encoding='utf-8')
        headers_json_bytes_len = len(headers_json_bytes)
        headers_json_bytes_len_pack = struct.pack('i', headers_json_bytes_len)
        conn.send(headers_json_bytes_len_pack)
        conn.sendall(headers_json_bytes)
        conn.sendall(ret_cmd)

socketserver模块

socketserver 处理多并发的tcp请求

在整个socketserver这个模块中,其实就干了两件事情

  1. 一个是循环建立链接的部分,每个客户链接都可以连接成功
  2. 一个通讯循环的部分,就是每个客户端链接成功之后,要循环的和客户端进行通信。
    server = socketserver.ThreadingTCPServer(ip_port, MyServer)

基于tcp的socketserver我们自己定义的类中的

  • self.server即套接字对象
  • self.request即一个链接
  • self.client_address即客户端地址

基于udp的socketserver我们自己定义的类中的

  • self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象)
    如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  • self.client_address即客户端地址

源码

查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer
流程:

  1. 实例化得到server,先找ThreadMinxIn中的__init__方法,发现没有init方法,然后找类ThreadingTCPServer的__init__,在TCPServer中找到,在里面创建了socket对象,进而执行server_bind(相当于bind),server_active(点进去看执行了listen)
  2. 找server下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
  3. 执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)
  4. 在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)
    上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....

示例代码

# client
import socket

client = socket.socket()
client.connect(('127.0.0.1', 8899))
while True:
    client_data = input('client>>>')
    client.send(client_data.encode('utf-8'))
    if client_data == 'q':
        break
    ret = client.recv(1024).decode('utf-8')
    print(ret)
client.close()

# server
import socketserver


# 定义一个类
class MyServer(socketserver.BaseRequestHandler):  # 2 类里面继承socketserver.BaseRequestHandler
    def handle(self):
        while True:
            # self.request  # conn连接
            from_client_data = self.request.recv(1024).decode('utf-8')
            if from_client_data == 'q':
                break
            print(from_client_data)
            server_input = input('server>>>')
            self.request.send(server_input.encode('utf-8'))
        self.request.close()


if __name__ == '__main__':
    # 服务端的IP地址和端口
    ip_port = ('127.0.0.1', 8899)
    # 设置allow_reuse_address允许服务器重用地址
    socketserver.TCPServer.allow_reuse_address = True
    # 绑定IP地址和端口,并且启动我定义的上面这个类

    server = socketserver.ThreadingTCPServer(ip_port, MyServer)
    # 让server永远运行下去,除非强制停止程序
    server.serve_forever()

验证客户端连接的合法性+hmac模块

hmac模块

hmac模块,内置模块,实现简单的网络编程中的客户端合法性验证
这实际上就是Hmac算法:Keyed-Hashing for Message Authentication。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。
和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。

import hmac
import os
import hashlib

ret = os.urandom(32)  # 生成一个32位的随机字节
hmac_obj = hmac.new(b'123', ret, 'md5')
print(hmac_obj.digest())
# 另一种方法实现
ret = os.urandom(32)
md5 = hashlib.md5(b'123')
md5.update(ret)
print(md5.digest())

socket+hmac实现验证客户端连接的合法性

# client
from socket import *
import hmac

secret_key = b'Jedan has a big key!'


def conn_auth(conn):
    """
    验证客户端到服务器的链接
    :param conn:
    :return:
    """
    msg = conn.recv(32)
    h = hmac.new(secret_key, msg)
    digest = h.digest()
    conn.sendall(digest)


def client_handler(ip_port, bufsize=1024):
    tcp_socket_client = socket(AF_INET, SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data = input('>>: ').strip()
        if not data: continue
        if data == 'quit': break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone = tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()


if __name__ == '__main__':
    ip_port = ('127.0.0.1', 9999)
    bufsize = 1024
    client_handler(ip_port, bufsize)

# server
from socket import *
import hmac
import os

secret_key = b'Jedan has a big key!'


def conn_auth(conn):
    """
    认证客户端链接
    :param conn:
    :return:
    """
    msg = os.urandom(32)  # 生成一个32字节的随机字符串
    conn.sendall(msg)
    h = hmac.new(secret_key, msg)
    digest = h.digest()
    respone = conn.recv(len(digest))
    return hmac.compare_digest(respone, digest)


def data_handler(conn, bufsize=1024):
    if not conn_auth(conn):
        print('该链接不合法,关闭')
        conn.close()
        return
    print('链接合法,开始通信')
    while True:
        data = conn.recv(bufsize)
        if not data: break
        conn.sendall(data.upper())


def server_handler(ip_port, bufsize, backlog=5):
    """
    只处理链接
    :param ip_port:
    :param bufsize:
    :param backlog:
    :return:
    """
    tcp_socket_server = socket(AF_INET, SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn, addr = tcp_socket_server.accept()
        print('新连接[%s:%s]' % (addr[0], addr[1]))
        data_handler(conn, bufsize)


if __name__ == '__main__':
    ip_port = ('127.0.0.1', 9999)
    bufsize = 1024
    server_handler(ip_port, bufsize)

文件上传下载

实现不粘包的文件上传下载

# client
import socket
import struct
import os
import json

default_download_path = os.getcwd()
buffer = 1024
sk = socket.socket()
try:
    sk.connect(('127.0.0.1', 8080))
except Exception as e:
    print('连接服务器失败', e)
    exit(1)
headers = {'option': None, 'file_name': None, 'file_size': None, 'path_upload': None, 'download_path': None}
while True:
    choose = input('>>>1.上传文件\n>>>2,下载文件\n>>>退出请输入q\n>>>')
    if choose == '1':
        path = input('输入要上传的文件路径')
        if not os.path.isfile(path):
            print('上传文件路径错误')
            continue
        path_upload = input('上传到指定路径')
        file_name = os.path.basename(path)
        file_size = os.path.getsize(path)
        headers['option'] = choose
        headers['file_name'] = file_name
        headers['file_size'] = file_size
        headers['path_upload'] = path_upload
        headers_json = json.dumps(headers)
        headers_json_bytes = bytes(headers_json, encoding='utf-8')
        headers_json_bytes_len = struct.pack('i', len(headers_json_bytes))
        sk.send(headers_json_bytes_len)  # 4字节,报文的长度
        sk.send(headers_json_bytes)  # 发送报文
        with open(path, 'rb') as f:
            content = f.read()
        sk.send(content)  # 发送文件
    elif choose == '2':
        headers['option'] = choose
        sk.send(struct.pack('i', len(bytes(json.dumps(headers), encoding='utf-8'))))
        sk.send(bytes(json.dumps(headers), encoding='utf-8'))
        file_list = json.loads(sk.recv(9216), encoding='utf-8')
        while True:
            print('文件列表')
            for key, item in enumerate(file_list):
                print(key, item)
            num = input('请选择文件序号')
            if num.isdigit() and int(num) in range(0, len(file_list)):
                break
        headers['file_name'] = file_list[int(num)][0]
        headers['file_size'] = file_list[int(num)][1]
        download_path = input('请输文件存放位置')
        if not os.path.isdir(download_path):
            download_path = default_download_path
        headers['download_path'] = download_path
        headers_json = json.dumps(headers)
        headers_json_bytes = bytes(headers_json, encoding='utf-8')
        headers_json_bytes_len = struct.pack('i', len(headers_json_bytes))
        sk.send(headers_json_bytes_len)  # 4字节,报文的长度
        sk.send(headers_json_bytes)  # 发送报文
        download_file_size = headers.get('file_size')
        download_file_path = os.path.join(headers.get('download_path'), headers['file_name'])
        with open(download_file_path, 'wb') as f:
            while download_file_size > 0:
                if download_file_size > buffer:
                    content = sk.recv(buffer)
                    download_file_size -= buffer
                else:
                    content = sk.recv(download_file_size)
                    download_file_size -= download_file_size
                f.write(content)
    elif choose == 'q':
        headers['option'] = choose
        headers_json = json.dumps(headers)
        headers_json_bytes = bytes(headers_json, encoding='utf-8')
        headers_json_bytes_len = struct.pack('i', len(headers_json_bytes))
        sk.send(headers_json_bytes_len)  # 4字节,报文的长度
        sk.send(headers_json_bytes)  # 发送报文
        break
    else:
        print('错误')
sk.close()

# server
import socket
import struct
import json
import os
default_path = os.getcwd()
buffer = 1024
download_directory = 'F:\迅雷下载'
sk = socket.socket()
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()
while True:
    msg = conn.recv(4)
    headers_json_bytes_len = struct.unpack('i', msg)[0]
    headers_json_bytes = conn.recv(headers_json_bytes_len)
    headers_json = headers_json_bytes.decode('utf-8')
    headers = json.loads(headers_json)
    option = headers.get('option')
    if option == '1':
        if os.path.isdir(headers.get('path_upload')):
            path = os.path.join(headers.get('path_upload'), headers.get('file_name'))
        else:
            path = os.path.join(default_path, headers.get('file_name'))
        print(path)
        file_size = headers.get('file_size')
        with open(path, 'wb') as f:
            while file_size > 0:
                if file_size >= buffer:
                    content = conn.recv(buffer)
                    file_size -= len(content)
                else:
                    content = conn.recv(file_size)
                    file_size -= file_size
                f.write(content)
    elif option == '2':
        # 下载文件,  发送下载文件的请求,服务器返回文件列表,选择一个文件,发送给服务器,服务器传送回文件
        file_list = os.listdir(download_directory)
        for key, path in enumerate(file_list):
            path_ = os.path.join(download_directory, path)
            if os.path.isfile(path_):
                file_size = os.path.getsize(path_)
            else:
                file_size = None
            file_list[key] = (path, file_size)
        conn.send(bytes(json.dumps(file_list), encoding='utf-8'))
        msg = conn.recv(4)
        headers_json_bytes_len = struct.unpack('i', msg)[0]
        headers_json_bytes = conn.recv(headers_json_bytes_len)
        headers_json = headers_json_bytes.decode('utf-8')
        headers = json.loads(headers_json)
        download_path = os.path.join(download_directory, headers['file_name'])
        with open(download_path, 'rb') as f:
            conn.send(f.read())
    elif option == 'q':
        break
conn.close()
sk.close()

posted @ 2018-11-14 20:53  写bug的日子  阅读(128)  评论(0编辑  收藏  举报