学习内容

  • 了解socket基本概念
  • 利用socket类方法获取主机、网络及目标服务的信息
  • UDP、TCP客户端/服务器编写
  • 案例:python SOCKET实现RSA加密的全双工聊天程序

学习步骤

构建python环境

同时开发多个python应用程序(共用一个Python,不同版本的包不兼容会产生无用包),利用virtualenv创建“Python虚拟环境”(小型、独立的、隔离功能的Python环境),从而避免产生无用包

  • 利用virtualenvwrapper管理virtualenv虚拟环境
    • mkvirtualenv venv :创建虚拟环境venv
    • workon :查看当前已有虚拟环境目录
    • workon venv :进入venv虚拟环境
    • deactivate :退出虚拟环境
    • rmvirtualenv venv:删除虚拟环境venv

socket概念

  • 运行在不同机器上的进程通过套接字发送报文来进行通信,套接字充当了两个进程通信的“中间人”,观察下图(OSI模型中):
  • socket
  • 套接字是个通信端点,操作系统使用整数来标识套接字,Python使用socket.socket对象表示套接字(该对象内部表示的是操作系统标识套接字的整数,可利用fileno()方法查看),调用socket.socket对象的方法请求使用套接字的系统调用是,该对象会自动使用内部维护的套接字整数标识符
  • socket.socket对象的fileno()方法
1
2
3
4
>>> import socket
>>> s = socket.socket()
>>> s.fileno()
3
  • IP地址、端口号
    • 端口号(port)传输层协议内容、用来标识一个进程
    • 一个端口号只能被一个进程占用
    • IP地址 + 端口号能标识网络上的某一台主机的某一个进程
  • 套接字组成:IP地址和端口号就构成了一个网络中的唯一标识符,即套接字
  • 套接字类型(常用的两种)
    • 流套接字:创建socket对象时(用socket.SOCK_STREAM)
      • 面向连接、可靠的数据传输服务。能够保证数据无差错、无重复、按顺序发送
    • 数据包套接字:创建socket对象时,使用socket.SOCK_DGRAM
      • 提供无连接服务。无需建立连接,只需将目的地址信息打包后发送;该服务使用UDP进行传输,延迟小且效率高,缺点不能保证数据传输的可靠性

利用socket类方法获取主机、网络及目标服务的信息

  • 获取主机名、地址

    1
    2
    3
    4
    5
    >>> import socket
    >>> socket.gethostname()
    'fishmouse'
    >>> socket.gethostbyname(_)
    '127.0.1.1'
  • 获取远程设备IP地址(如获取:www.baidu.com)

    1
    2
    >>> socket.gethostbyname('www.baidu.com')
    '14.215.177.38'

    可看到socket.gethostbyname具有==域名解析的作用==,ping一下看通不通

    1
    2
    3
    4
    5
    (venv) yuhao@fishmouse:~/Envs/venv/project$ ping 14.215.177.38
    PING 14.215.177.38 (14.215.177.38) 56(84) bytes of data.
    64 bytes from 14.215.177.38: icmp_seq=1 ttl=55 time=37.9 ms
    64 bytes from 14.215.177.38: icmp_seq=2 ttl=55 time=39.4 ms
    64 bytes from 14.215.177.38: icmp_seq=3 ttl=55 time=34.8 ms
  • IP地址格式转换(打包成32位二进制格式):socket类方法inet_aton、inet_ntoa

    • inet_aton()使用

      1
      2
      3
      4
      5
      6
      >>> ip_addr ='127.0.0.1'
      >>> socket.inet_aton(ip_addr)
      b'\x7f\x00\x00\x01'
      >>> import binascii
      >>> binascii.hexlify(_)
      b'7f000001'

    观察看到,转换后的32位二进制格式,并调用binasci.hexlify以16进制形式表示二进制数据

    10进制结果,单个字节转换

    1
    2
    3
    >>>import struct
    >>> struct.unpack('B',b'\x7f')[0]
    127
    • inet_ntoa()使用:32位二进制包转换为IPv4地址

      1
      2
      3
      4
      5
      a的主机字节序----------网络字节序 ---------b的主机字节序>>> ip_addr ='127.0.0.1'
      >>> socket.inet_aton(ip_addr)
      b'\x7f\x00\x00\x01'
      >>> socket.inet_ntoa(_)
      '127.0.0.1'
  • 通过指定的端口和协议找到服务名

    • socket.getservbyport()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      >>> socket.getservbyport(80)
      'http'
      >>> socket.getservbyport(53)
      'domain'
      >>> socket.getservbyport(25)
      'smtp'
      >>> socket.getservbyport(21)
      'ftp'
      >>> socket.getservbyport(3306)
      'mysql'
    • socket.getservbyname() :通过服务名获取端口

    1
    2
    3
    4
    >>> socket.getservbyname('ssh')
    22
    >>> socket.getservbyname('http')
    80

    linux系统中,etc/services文件中可查看相关服务和端口

  • 主机字节序和网络字节序之间的转换

    不同主机a,b之间通信,数据格式需转换

    a的固有数据存储——-标准化——–转化成b的固有格式

    也即为:

    ==a的主机字节序———-网络字节序 ———b的主机字节序==

    • 主机字节序

      主机内部,内存中数据的处理方式,可以分为两种:

      • 大端字节序:按照内存的增长方向,高位数据存储在高位内存中

      • 小端字节序:按照内存的增长方向,高位数据存储在低位内存中

    • socket.ntohl()、socket.htonl()、ntohs()、htons()

1
2
3
4
5
6
7
8
9
10
11
>>> data = 1234
>>> socket.htonl(data)
3523477504
>>> socket.htons(data)
53764
>>> socket.ntohl(data)
3523477504
>>> socket.ntohs(data)
53764
>>> socket.ntohs(53764)
1234
  • 设定并获取默认的套接字超时时间

    • socket.gettimeout()、socket.settimeout()

      1
      2
      3
      4
      5
      >>> s = socket.socket()
      >>> s.gettimeout()
      >>> s.settimeout(100)
      >>> s.gettimeout()
      100.0

      默认套接字超时时间为0

  • 套接字错误异常处理

    • try…except 套接字异常类型 as 参数…
  • argparse:命令项选项与参数解析的模块

    • parser=argparse.ArgumentParser():创建解析对象
    • parser.add_argument():向对象中添加关注的命令行参数和选项
    • given_args = parser.parse_args():对象解析
  • 套接字发送和接收的缓冲区大小修改

    • socket中getsockopt()、setsockopt()方法
  • 套接字阻塞模式和非阻塞模式

    • s= socket.socket()
    • s.setblocking(1):设为阻塞模式
    • s.setblocking(0):设为非阻塞模式

    默认情况下,TCP套接字处于阻塞模式

UDP

  • 多路复用:允许多个会话共享同一介质或机制的一种解决方案

  • UDP支持多路复用:UDP协议提供端口号,用于对目标为同一机器上不同服务的多个数据包进行适当的多路分解

  • TCP:多路复用、可靠传输

  • UDP机制:仅使用IP地址和端口进行标识,以此将数据包发送至目标地址

  • 使用自环接口的UDP服务器和 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    # UDP client and server on localhost
    # 814udp_local.py
    import argparse, socket
    from datetime import datetime

    MAX_BYTES = 65535

    # server
    def server(port):
    sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    sock.bind(('127.0.0.1',port))

    # getsockname()返回sock示例对象的(地址,端口)
    print("Listening at {}".format(sock.getsockname()))
    while True:
    data, address = sock.recvfrom(MAX_BYTES)
    text = data.decode('ascii')

    print("The client at {} says {!r}".format(address,text))

    text = 'Your data was {} bytes long '.format(len(data))
    data = text.encode('ascii')
    sock.sendto(data,address)

    # 客户端
    def client(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    text = 'The time is {}'.format(datetime.now())
    data = text.encode('ascii')

    sock.sendto(data,('127.0.0.1',port))

    # sock.getsockname()获取当前进程的(地址,端口)元组信息
    print('The OS assigned me the address {}'.format(sock.getsockname()))
    data, address = sock.recvfrom(MAX_BYTES)

    text = data.decode()
    # format格式化字符串
    print('The server {} replied {!r}'.format(address,text))


    if __name__ == '__main__':
    # 字典
    choices = {'client':client,'server':server}
    # 创建参数解析对象
    parser = argparse.ArgumentParser(description='Send and receive UDP locally')
    #添加要解析的参数
    parser.add_argument('role',choices=choices,help ='which role to play')
    parser.add_argument('-p',metavar='PORT',type=int,default=1060,help='UDP port (default 1060)')
    # 参数解析
    args = parser.parse_args()

    # 调用服务端或客户端函数
    function = choices[args.role]
    function(args.p)
    • 先运行服务端

      • python 814udp_local.py server

        结果:

        Listening at (‘127.0.0.1’, 1060)
        The client at (‘127.0.0.1’, 60945) says ‘The time is 2019-08-16 16:34:56.276877’

    • 再运行客户端

      • python 814udp_local.py client

        The OS assigned me the address (‘0.0.0.0’, 60945)
        The server (‘127.0.0.1’, 1060) replied ‘Your data was 38 bytes long ‘

    • 混杂客户端与垃圾回复

      814udp_local.py代码中,客户端程序存在安全隐患,如fg果服务端响应延迟一会,攻击者伪装成服务器的一个响应,客户端并没有检查是否是真正服务器的响应

      • 先运行服务器,再将服务器暂停,创建一个快速发送信息的响应给客户端,再==fg命令==将暂停的服务器开启

      • 客户端

        观察到,客户端收到的数据实际上是伪装的数据,真正的服务器的响应没到客户端

      • 混杂客户端

        不考虑地址是否正确,接收并处理所有收到的数据包的网络监听客户端在技术上叫 作混杂( promiscuous )客户端

python SOCKET实现RSA加密的全双工聊天程序实现

  • 题目背景

    RSA加密解密是利用非对称秘钥解决传输过程中机密性的问题,将之用在聊天程序上,其中使用rsa模块,发送方生产公钥和私钥,然后使用公钥将信息加密后,利用pickle模块封装加密后的消息和私钥,然后发送给接收方,接收方同样通过pickle模块将消息进行解封,使用发送过来的私钥将消息解密,并将内容打印在屏幕上

  • 题目要点

    • 传输协议:TCP套接字创建客户端和服务端_

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

    • 数据传输格式:pickle序列化数据

      • pickle.dumps()序列化

      • pickle.loads()反序列化

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        >>> import pickle
        >>> encryptdata = 'hello'
        >>> key = 882321
        >>> message = pickle.dumps([encryptdata,key])
        >>> type(message)
        <class 'bytes'>
        >>> message
        b'\x80\x03]q\x00(X\x05\x00\x00\x00helloq\x01J\x91v\r\x00e.'
        >>> origndata = pickle.loads(message)
        >>> origndata
        ['hello', 882321]
    • 数据加密方式:RSA加解密

      1
      2
      3
      4
      5
      6
      7
      8
      9
      >>>data = 'hello'
      >>> import rsa
      >>> (PubKey,PrivateKey) = rsa.newkeys(512)
      >>> encryptdata = rsa.encrypt(data.encode(),PubKey)
      >>> encryptdata
      b'=\\\x1c\x93]^(Z/\xac\x81\xfd\xffj!\x0b:r\xb0\x1b\xf9\x97VZ\xdf\xe1\x9e2\xb4\x05G4\x01\x9f\xc8\xfd\x1e\x00\xa1\xb7\xbdU\x98\xbc\x1e5\xa1yy\xee$\xcd\xf8\x10\xf4\xba\t\x84\xba\x13\x99hs\x8d'
      >>> decryptdata = rsa.decrypt(encryptdata,PrivateKey)
      >>> decryptdata
      b'hello'
    • 题目图解

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    import rsa
    import socket
    import threading
    import pickle

    PORT = 4396
    BUFF = 1024


    def RsaEncrypt(str):
    # 利用rsa产生公钥、私钥
    (PubKey, PrivateKey) = rsa.newkeys(512)
    content = str.encode('utf8')
    # 使用公钥加密
    Encrypt_Str = rsa.encrypt(content, PubKey)
    # 返回加密信息和私钥
    return (Encrypt_Str, PrivateKey)


    def RsaDecrypt(str, pk):
    Decrypt_Str = rsa.decrypt(str, pk)
    Decrypt_Str_1 = Decrypt_Str.decode('utf8')
    return Decrypt_Str_1


    def SendMessage(Sock, test):
    while True:
    SendData = input()
    # 加密要发送的数据
    (encryptdata, PrivateKey) = RsaEncrypt(SendData)

    # 打印加密后的数据
    print('encrypted data is ' + str(encryptdata))

    # pickel封装加密后的数据和私钥
    Message = pickle.dumps([encryptdata, PrivateKey])
    if len(SendData) > 0:
    Sock.send(Message)

    def RecvMessage(Sock, test):
    while True:
    # 接收数据
    Message = Sock.recv(BUFF)
    # pickle解封数据
    (recvdata, PrivateKey) = pickle.loads(Message)
    # 对加密的数据解密
    decryptdata = RsaDecrypt(recvdata, PrivateKey)
    if len(Message)>0:
    print("receive message:" + decryptdata)


    def main():
    type = input('please input server or client:')
    if type == 'server':
    # 创建套接字
    ServerSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # 端口绑定
    ServerSock.bind(('127.0.0.1',PORT))
    # 服务器允许连接的个数
    ServerSock.listen(5)
    print("listening......")
    while True:
    ConSock,addr = ServerSock.accept()
    print('connection succeed' + '\n' + 'you can chat online')
    # 多线程运用
    thread_1 = threading.Thread(target = SendMessage, args = (ConSock, None))
    thread_2 = threading.Thread(target = RecvMessage, args = (ConSock, None))
    thread_1.start()
    thread_2.start()
    elif type == 'client':
    ClientSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ServerAddr = input("please input the server's ip address:")
    ClientSock.connect((ServerAddr, PORT))
    print('connection succeed, chat start!')
    thread_3 = threading.Thread(target = SendMessage, args = (ClientSock, None))
    thread_4 = threading.Thread(target = RecvMessage, args = (ClientSock, None))
    thread_3.start()
    thread_4.start()


    if __name__ == '__main__':
    main()
  • 结果演示

    • 先启动server进行监听

    • 启动客户端连接server

    • 客户端向服务器发送消息

    • 服务器端接收到消息,并将消息打印在屏幕上

  • 分析

    综上,该程序利用的是TCP套接字保证了传输的可靠性,并利用多线程进行信息交互,pickle封装数据,rsa产生公钥、私钥、和加/解密等操作