Advertisement

Python 绝技 —— TCP服务器与客户端

阅读量:

i春秋作家:wasrehpic

0×00 前言

「互联网」长期以来一直是黑客最热衷的竞技场。数据在任意时刻都会在网络中任意传播:通过主机扫描攻击、代码注入攻击、网络嗅探手段获取信息、实施数据篡改与重放操作以及发起拒绝服务攻击等手段进行全方位渗透……可见,在技术日益发达的今天,想要做好网络安全防护工作就必须要掌握这些技能。

Python 被广泛认为是解释型脚本语言,在其诞生以来的时间里发展成熟,并凭借其简明扼要且易于理解的特点广受黑客群体的喜爱。特别在网络工具开发中无需深入处理底层复杂的编程细节,并不追求运行效率的高度优化;因此它成为安全工作者不可或缺的强大工具。

本文作为《Python绝技》系列工具文章的开篇。首先介绍互联网的核心通信协议——TCP协议。进一步以Python的socket模块为例讲解网络套接字。最后提供了一个基于TCP协议的服务器与客户端之间的Python脚本,并演示了两者的通信过程。

0×01 TCP 协议

该协议(Transmission Control Protocol, Transmission Control Protocol for Network Data Transfer)是一种基于连接模型、支持稳定数据传输并采用分段传输技术的网络通信协议。

TCP协议运行可分为建立连接、数据传输以及断开连接三个环节,在这些过程中最引人注目的是TCP协议三次握手(TCP Three-way Handshake),这也是全面解析本文中所涉及的TCP服务器与客户端通信机制的基础

连接创建(Connection Establishment)

所谓的「三次握手」指的是 TCP 服务器与客户端成功建立通信连接必经的三个步骤 共需通过三个报文完成

Handshake Step 1

客户端通过网络向服务器发送SYN报文以发起连接请求。(SYN字段值为1)在此次报文中,默认情况下已设置初始序列号seq=x和确认号ack=0。

Handshake Step 2

当服务器接收到客户端的一方发送来的SYN报文时,在数据链路层完成相关操作后会立即传输一个ACK+SYN报文(其中ACK字段设为1、SYN字段设为1),以响应该连接请求并同时向其发起新的连接建立请求。此时数据包中的序列编号字段设为y、相应字段设为x加一。

Handshake Step 3

客户端响应服务器的SYN报文后,在线程中提交ACK报文(ACK=1)以确认服务器的连接请求。其中该报文中包含的序列为seq=x+1,并且对应的acks字段值为y+1。

对于上述过程的理解,需要注意以下几点:

  • 在TCP协议头部中的标记符字段部分定义了报文的功能信息。该字段占据第104至第111位共计8位二进制数。每一位二进制数代表一种特定的功能状态:置"1"表示该功能处于开启状态;置"0"则表示功能被关闭状态。例如,在SYN报文中其标志字段值为 0b1 余下按要求处理
  • 位于TCP协议头部中的序列号字段部分定义了报文的序贯编号信息。此字段占据第32至第63位共计32位二进制数长度。在整个"三步握手"过程中初始值由数据发送方随机产生。
  • 位于TCP协议头部中的确认号字段部分定义了报文的认可编号信息。此字段占据第64至第95位共计32位二进制数长度且仅存在于ACK+SYN型综合报告信令中其值等于上一个接收单位所收到信令报告的具体序贯编号加一。

为了更方便地理解,下面给出一张 TCP 协议三次握手的示意图:

[

image.png

](http://image.3001.net/images/20180525/15272324843613.png)

0×02 Network Socket

Network Socket(网络连接器)是计算机网络中多程数据传输的端点节点。也可理解为操作系统提供的进程间通信机制。

进程间通信(Inter-Process Communication,IPC)的根本前提是能够唯一标示每个进程。在本地主机的进程间通信中,可以用 PID(进程 ID)唯一标示每个进程,但 PID 只在本地唯一,在网络中不同主机的 PID 则可能发生冲突,因此采用「IP 地址 + 传输层协议 + 端口号」的方式唯一标示网络中的一个进程。

小贴士:网络层中的 IP 地址可用于精确标识主机;传输层中的 TCP/UDP 协议及其端口号可单独标识该主机运行的一个进程。值得注意的是,在同一台主机上,TCP协议与UDP协议之间可能采用相同的端口号。

各种能够支持网络通信的编程语言各自都设有相应的 socket API 接口。例如,在 Python 3 中, 我们将详细阐述服务器与客户端通过 TCP 协议进行通信连接的具体交互流程:包括如何建立连接、发送接收数据以及处理超时等常见操作。

[

image.png

](http://image.3001.net/images/20180525/15272325019173.png)

Initially forming impressions of the brainstorming process, which makes it more straightforward to comprehend the next sections about TCP server and client implementation in Python.

0×03 TCP 服务器

复制代码
    #!/usr/bin/env python3# -*- coding: utf-8 -*-importimportdef tcplink(conn, addr):"Accept new connection from %s:%s"b"Welcome!\n"whileTrueb"What's your name?"1024if"exit"b"Good bye!\n"breakb"Hello %s!\n""Connection from %s:%s is closed""127.0.0.1"60005"Waiting for connection..."whileTrue
  • Line 6:建立tcplink()函数,并为该函数提供两个参数:第一个参数为服务器与客户端交互数据的套接字对象conn(socket object),第二个参数为客户端的IP地址与端口号组成的二元组addr (host, port)。
    • Line 8:在连接成功后立即发送问候信息:"欢迎光临!\n"。
    • Line 9:进入处理客户端交互数据的循环过程。
    • Line 10:向客户端发送问候询问信息:"What's your name?"。
    • Line 11:从客户端读取非空的数据字符串。
    • Line 12:如果接收到的数据字符串为"exit"则发送退出信息:"Good bye!\n"并终止对客户端交互数据的处理流程。
    • Line 15:如果接收到的数据字符串不等于"exit"则发送问候信息:"Hello %s!\n"其中% s表示来自客户端的数据字符串。
    • Line 16:关闭套接字不再向客户端发送任何信息。
    • Line 19:创建socket对象并指定使用IPv4协议AF_INET以及TCP协议SOket.SOCK_STREAM。
      然后绑定该socket到本地主机地址("127.0.0.1",6000),开启监听功能等待客户端连接请求。
      进入监听请求处理循环:
      接收客户的连接请求获取对应的套接字对象conn和客户提供的IP地址与端口号addr(其中addr以二元组形式存储).
      利用多线程技术启动新线程用于处理每个客户的TCP连接请求,
      实现服务器同时支持多个客户端同时通信的功能,
      启动新线程以处理当前客户的连接请求.

0×04 TCP 客户端

复制代码
    #!/usr/bin/env python3# -*- coding: utf-8 -*-import"127.0.0.1"60001024"client"whileTrueif1024"Please input your name: "ifnotcontinue1024if"exit"break
  • Line 5:创建 socket 对象,第一个参数为 socket.AF_INET,代表采用 IPv4 协议用于网络通信,第二个参数为 socket.SOCK_STREAM,代表采用 TCP 协议用于面向连接的网络通信。
    • Line 6:向 (“127.0.0.1″, 6000) 主机发起连接请求,即本地主机的 TCP 6000 端口。
    • Line 7:连接成功后,接收服务器发送过来的问候信息 "Welcome!\n"
    • Line 9:创建一个非空字符串变量 data,并赋初值为 "client"(只要是非空字符串即可),用于判断是否接收来自服务器发来的询问信息 "What's your name?"
    • Line 10:进入与服务器交互数据的循环阶段。
    • Line 12:当用户的输入非空且不等于 "exit"(记为非法字符串)时,则接收服务器发来的询问信息。
    • Line 13:要求用户输入名字,一条合法字符串即可。
    • Line 14:当用户输入非空,则重新开始循环,要求用户重新输入合法字符串。
    • Line 16:当用户输入合法字符串时,则将字符串转换为 bytes 对象后发送至服务器。
    • Line 17:接收服务器的响应数据,并将 bytes 对象转换为字符串后打印输出。
    • Line 18:当用户输入字符串 "exit" 时,则结束与服务器交互数据的循环阶段,即将关闭套接字。
    • Line 21:关闭套接字,不再向服务器发送数据。

0×05 TCP 进程间通信

为TCP服务器与客户端的脚本分别起名为tcp_server.py与tcp_client.py后放置于桌面上,笔者将在Windows 10系统上使用PowerShell进行演示

提醒读者在调试时,请确保本机已安装 Python 3,并特别提醒作者已经更改了默认启动路径名为 python 成为 python3

单服务器 VS 单客户端

[

image.png

](http://image.3001.net/images/20180525/15272325257345.png)

  1. 在其中一个 PowerShell 中运行命令 python3 ./tcp_server.py,服务器显示 Waiting for connection...,并监听本地主机的 TCP 6000 端口,进入等待连接状态;
  2. 在另一个 PowerShell 中运行命令 python3 ./tcp_client.py,服务器显示 Accept new connection from 127.0.0.1:42101,完成与本地主机的 TCP 42101 端口建立通信连接,并向客户端发送问候信息与询问信息,客户端接收到信息后打印输出;
  3. 若客户端向服务器发送字符串 AliceBob,则收到服务器的问候响应信息;
  4. 若客户端向服务器发送空字符串,则要求重新输入字符串;
  5. 若客户端向服务器发送字符串 exit,则收到服务器的结束响应信息;
  6. 客户端与服务器之间的通信连接已关闭,服务器显示 Connection from 127.0.0.1:42101 is closed,并继续监听客户端的连接请求。

单服务器 VS 多客户端

[

image.png

](http://image.3001.net/images/20180525/15272325366206.png)

  1. 在其中一个 PowerShell 中运行命令 python3 ./tcp_server.py,服务器显示 Waiting for connection...,并监听本地主机的 TCP 6000 端口,进入等待连接状态;
  2. 在另三个 PowerShell 中分别运行命令 python3 ./tcp_client.py,服务器同时与本地主机的 TCP 42719、42721、42722 端口建立通信连接,并分别向客户端发送问候信息与询问信息,客户端接收到信息后打印输出;
  3. 三台客户端分别向服务器发送字符串 Client1Client2Client3,并收到服务器的问候响应信息;
  4. 所有客户端分别向服务器发送字符串 exit,并收到服务器的结束响应信息;
  5. 所有客户端与服务器之间的通信连接已关闭,服务器继续监听客户端的连接请求。

0×06 Python API Reference

socket 模块

本节将阐述上述代码中所涉及的主要函数(socket 模块内置函数),这些核心组件之一在socket编程中扮演着关键角色。

socket() 函数

socket()函数用于创建网络通信中的套接字对象。详细说明其功能和参数。

复制代码
    0None
  • family 参数代表地址族(Address Family),默认值为 AF_INET,用于 IPv4 网络通信,常用的还有 AF_INET6,用于 IPv6 网络通信。family 参数的可选值取决于本机操作系统。
    • type 参数代表套接字的类型,默认值为 SOCK_STREAM,用于 TCP 协议(面向连接)的网络通信,常用的还有 SOCK_DGRAM,用于 UDP 协议(无连接)的网络通信。
    • proto 参数代表套接字的协议,默认值为 0,一般忽略该参数,除非 family 参数为 AF_CAN,则 proto 参数需设置为 CAN_RAW 或 CAN_BCM。
    • fileno 参数代表套接字的文件描述符,默认值为 None,若设置了该参数,则其他三个参数将会被忽略。

socket 对象

此小节阐述上述代码中涉及的对象级别内建功能,也属于socket编程中的核心功能之一。值得注意的是,在以下内容中提到的关键特性主要体现在其具体实现细节上。

bind() 函数

bind函数用于将套接字对象与指定的IP地址和端口号绑定在一起。需要注意的是,在调用该函数之前,请确保套接字对象尚未进行过任何绑定操作,并且对应的端口号也未被当前系统所占用以避免程序运行时的错误信息提示。函数的基本框架如下所示:

复制代码
  • address参数用于指定与套接字绑定的地址。其格式由套接字的family参数决定。当family参数设置为AF_INET时,address参数将被表示为一个二元组(host, port)。其中host字段以字符串形式存储主机地址信息(port字段则以整数形式存储端口号数值)

listen() 函数

listen() 函数用于实现为TCP服务器实现套接字监听功能。其函数体如下所示:

复制代码

backlog 是一个可选参数,在套接字拒绝新连接前能够挂起的最大允许连接数上具有一定的灵活性。通常建议将 backlog 设置为5;如果未指定,默认会进行合理配置以确保系统的负载平衡能力得到充分发挥。

connect() 函数

该函数负责将 TCP 客户端连接到 TCP 服务器。其具体实现形式如下:该函数接收一个 socket 对象作为参数,并返回连接建立后的 socket 对象。

复制代码

该变量用于标识与套接字连接所需的主机地址,并受套接字家族参数的影响。
当套接字家族参数设置为 AF_INET 时,则该变量以 (host, port) 的形式表示。

accept() 函数

accept()函数([socket.socket.accept])用于实现TCP服务器接收来自TCP客户端的连接请求。具体实现如下:

复制代码

accept()函数返回的是一个由(conn, address)构成的有序对。其中conn是服务器用来与客户端进行数据交互的通信套接字对象(address是客户端所使用的IP地址和端口号)用有序对(host,port)来表示

send() 函数

socket.send()实现了将数据传递至远程套接字目标的功能。需要注意的是,在本机套接字与远程套接字之间尚未建立连接之前无法调用该功能而会引发错误信息。由此可见,在TCP协议下的进程间通信中才可应用此函数,在UDP协议下则应使用socket.sendto()来完成数据传输操作。其基本架构如下所示:

复制代码
  • bytes 参数代表即将发送的 bytes 对象数据。例如,对于字符串 "hello world!" 而言,需要用 encode() 函数转换为 bytes 对象 b"hello world!" 才能进行网络传输。
  • flags 可选参数用于设置 send() 函数的特殊功能,默认值为 0,也可由一个或多个预定义值组成,用位或操作符 |隔开。详情可参考 Unix 函数手册中的 send(2),flags 参数的常见取值有 MSG_OOB、MSG_PEEK、MSG_WAITALL 等。

send() 函数的返回值是发送数据的字节数。

recv() 函数

该函数由远程连接实体读取数据。

复制代码
  • bufsize变量表示网络套接字的最大接收数据长度。特别提示,在确保硬件资源与网络流量最佳匹配的前提下,请将bufsize变量设置为2的幂次方形式(如4096),以便优化数据传输效率。
  • flags变量用于指定recv()函数的独特功能,默认设置为无状态信息。通常由一组预先定义的功能位组合而成,并通过位或操作符|连接起来实现多选功能。建议详细参阅《Unix系统调用 recv(2)》手册[https://linux.die.net/man/2/recv],其常见取值包括.MSG_OOB,.MSG_PEEK,.MSG_WAITALL等多种配置选项。

recv() 函数的返回值是接收的内容数据。例如,在接收到了 bytes 对象 b"hello world!" 的情况下,则最好采用 decode() 函数将其转换为字符串 "hello world!" 并打印输出以实现预期效果

close() 函数

close() 函数负责关闭本地套接字并释放与其相关联的所有资源。

复制代码

threading 模块

本节阐述上述代码中使用的 threading模块内部类及其在Python多线程编程中的核心地位。

Thread() 类

Thread() 类可用来创建线程对象,通过调用 start() 函数启动新线程。类原型如下:

Thread() 类可用来创建线程对象, 通过调用 start() 函数启动新线程. 类原型如下:

复制代码
    class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
  • group 参数将被用于 future 实现 ThreadGroup() 类的过程。
  • target 参数将指定被 run() 函数激活后调用的具体函数,默认情况下无函数将被调用。
  • name 参数决定线程名称,默认情况下系统会自动生成名称。
  • args 字段存储 target 指向函数的常规参数信息。
  • kwargs 字典存储 target 指向函数的关键字参数信息。
  • daemon 属性用于判断进程是否为守护进程。

threading 对象

本节将介绍上述代码中使用到的 threading 类内置函数,并作为多线程编程的关键组件存在。请注意,在以下函数原型中,“threading”一词代表的是一个线程类实例

start() 函数

该函数负责启动线程活动

复制代码

请特别注意以下事项:每个线程对象的 start() 函数只能被唯一调用一次;如果不这样做,则会触发 RuntimeError 错误。

0×07 总结

本文阐述了 TCP 协议及 socket 编程的基本概念。基于 Python 3 开发并演示了 TCP 服务器与客户端之间的通信流程。其中采用了基本的多线程技术来辅助开发。最后将脚本涉及的 Python API 整理为参考资料库,有助于深入理解整个实现流程。

本人能力尚浅,在本文中可能存在不足或错误的地方。建议大家直言不讳地提出批评指正。包涵无礼地提醒一下,请各位读者多多包涵,并欢迎各位读者前来交流技术细节。再次感谢您的阅读。

本文的相关参考请移步至:

简单理解Socket
TCP编程 – 廖雪峰的官方网站
多线程 – 廖雪峰的官方网站

遇到问题可以在论坛里交流哦!欢迎加入我们共同探讨 >>>点击跳转

全部评论 (0)

还没有任何评论哟~