TCP 编程
个人计算机或者服务器上通常会运行多个应用程序, 但是我们只需要一条网线就能连接互联网, 去访问互联网的资源。如果有一个公网的 IP 可以被其他用户访问到, 这是由于所有的程序都共享了这一个线路, 为了实现共享, TCP 通过把要发送的数据流分解成小的信息包在网络上传输, 这些信息包到了接收者那里会被重组。
IP 地址
TCP 协议要把信息包投递给正确的接收者, 需要识别对方的服务器, 并且知道和这个服务器上的哪个进程通信, 为了实现这个目的, 需要使用 IP 和端口。
IP 地址有 4 组数字, 每组数字最多 3 位, 中间用点隔开, 每组数字的范围是 0
-255
。
下面是几个 IP 地址的例子:
192.168.0.1 (私有 IP)
172.16.22.19 (私有 IP)
8.8.8.8
123.22.122.1
私有 IP 范围
下面三种范围内的 IP 地址都是私有的, 是在局域网里面可以随意使用的。而公网 IP 需要到电信运营商申请才可以使用。
10.0.0.0 - 10.255.255.255
172.16.0.0 - 172.31.255.255
192.168.0.0 - 192.168.255.255
端口号
每个程序都用到唯一的端口, 端口是一个数字, 从 1 - 65535。
1024 以下的端口号也叫做知名端口号, 是那些由互联网名称与数字地址分配机构(ICANN)预留给传输控制协议(TCP)的, 这些端口都代表了具体的约定的用法, 不应该滥用。剩下的端口号叫动态端口号或私有端口号, 比较自由, 可以按自己的业务需要约定使用。
IPv6
Ipv6 是网际协议(IP)的最新版本, 用作互联网的网络层协议, 用它来取代 IPv4 主要是为了解决 IPv4 地址枯竭等问题。
IPv6 二进制位下为 128 位长度, 以 16 位为一组, 每组以冒号(:)隔开, 可以分为 8 组, 每组以 4 位十六进制方式表示, 如下:
2001:0db8:85a3:08d3:1319:8a2e:0370:7344
DNS
由于 IP 地址是一堆数字, 不方便记忆, 所以通常使用域名来代替 IP 地址, 用户只需要输入域名就能访问对应的 IP 地址。保存域名和 IP 地址对应关系的网络服务就是 DNS。
路由
一旦应用程序请求操作系统向某一特定 IP 地址发送数据, 操作系统就需要决定如何使用该机器连接的某一个物理网络来传输数据。这个决定, 也就是根据目的 IP 地址选择将 IP 数据包发往何处, 即路由。
套接字(Socket)
socket 是一种操作系统提供的进程间通信机制。
它是网络通信过程中端点的抽象表示, 包含进行网络通信必须的 5 种信息:
-
连接使用的协议
-
本地主机的 IP 地址
-
本地进程的协议端口
-
对方主机的 IP 地址
-
对方进程的协议端口
简单的说, socket 就是通信双方的一种约定, 用 socket 相关的函数去完成通信过程。
在同一台计算机上, TCP 和 UDP 协议可以同时使用相同的端口而互相不会干扰(比如 DNS 就用到了 TCP 和 UDP, 端口都是 53), 操作系统根据 socket 的地址可以决定将数据送达特定的进程或线程。
套接字类型
可以把套接字理解为程序在本地或者跨互联网来回传递数据的通信通道的一个端点, 套接字有两个控制发送数据的基本属性:
-
地址家族: 控制 OSI 网络层的协议。
-
套接字(Socket) 类型: 控制传输层的协议。
下面是套接字(Socket)类型:
-
流式套接字(SOCK_STREAM)
。用于通过面向连接可靠的数据传输服务, 这个服务可以保证数据能够实现无差错无重复的发送, 并且按顺序地接收。流式套接字(SOCK_STREAM)之所以能够提供这种可靠的数据服务, 原因在于它使用了 TCP 协议。 -
数据报套接字(SOCK_DGRAM)
。提供了一种无连接的服务, 这个服务并不能保证数据传输的可靠性, 数据有可能在传输过程中丢失或者出现数据重复, 而且也无法保证顺序地接收数据。数据报套接字(SOCK_DGRAM)使用的是 UDP 协议, 对于有可能出现的数据丢失的问题, 需要在程序里面做相关的处理。 -
原始套接字(SOCK_RAW)
。允许对较底层的协议直接访问(比如 IP、ICMP 协议)。
套接字地址家族(Address Family)
Python 支持非常多的地址家族, 最常见的 3 种如下:
-
AF_INET
。表示 IPv4 的互联网寻址。 -
AF_INET6
。表示 IPv6 的互联网寻址。 -
AF_UNIX
。用于在同一个服务器上的进程间的通信。
使用 socket 模块
Python 中网络编程主要使用 socket
模块, 要创建一个套接字, 需要使用 socket.socket()
函数:
# 一般语法
socket(socket_family, socket_type, protocol=0)
# 其中, socket_family 是套接字地址家族: AF_UNIX、AF_INET、AF_INET6
# socket_type 是套接字类型: SOCK_STREAM 或 SOCK_DGRAM
# protocol(协议) 通常省略, 默认为 0
# 创建 TCP 套接字:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Socket 的方法
服务端使用方法
s.bind(address)
: 将套接字绑定到地址, 在 AF_INET 下, 以 tuple(host, port) 的方式传入, 如s.bind((host, port))。
s.listen(backlog)
: 开始监听 TCP 传入连接, backlog 指定在拒绝链接前, 操作系统可以挂起的最大连接数, 该值最少为 1, 大部分应用程序设为 5 就够用了。
s.accept()
: 接受 TCP 链接并返回(conn, address), 其中 conn 是新的套接字对象, 可以用来接收和发送数据, address 是链接客户端的地址。
客户端使用方法
s.connect(address)
: 链接到 address 处的套接字, 一般 address 的格式为 tuple(host, port), 如果链接出错, 则返回 socket.error 错误。
s.connect_ex(address)
: 功能与 s.connect(address) 相同, 但成功返回 0, 失败返回 errno 的值。
通用的方法
s.recv(bufsize[, flag])
: 接受 TCP 套接字的数据, 数据以字符串形式返回, buffsize 指定要接受的最大数据量, flag 提供有关消息的其他信息, 通常可以忽略。
s.send(string[, flag])
: 发送 TCP 数据, 将字符串中的数据发送到链接的套接字, 返回值是要发送的字节数量, 该数量可能小于 string 的字节大小。
s.sendall(string[, flag])
: 完整发送 TCP 数据, 将字符串中的数据发送到链接的套接字, 但在返回之前尝试发送所有数据。成功返回 None, 失败则抛出异常。
s.recvfrom(bufsize[, flag])
: 接受 UDP 套接字的数据, 与 recv() 类似, 但返回值是 tuple(data, address)。其中 data 是包含接受数据的字符串, address 是发送数据的套接字地址。
s.sendto(string[, flag], address)
: 发送 UDP 数据, 将数据发送到套接字, address 形式为 tuple(ipaddr, port), 指定远程地址发送, 返回值是发送的字节数。
s.close()
: 关闭套接字。
s.getpeername()
: 返回套接字的远程地址, 返回值通常是一个包含 ipaddr, port 的元组。
s.getsockname()
: 返回套接字自己的地址, 返回值通常是一个包含 ipaddr, port 的元组。
s.setsockopt(level, optname, value)
: 设置给定套接字选项的值。
s.getsockopt(level, optname[, buflen])
: 返回套接字选项的值。
s.settimeout(timeout)
: 设置套接字操作的超时时间, timeout 是一个浮点数, 单位是秒, 值为 None 则表示永远不会超时。一般超时期应在刚创建套接字时设置, 因为他们可能用于连接的操作, 如s.connect()。
s.gettimeout()
: 返回当前超时值, 单位是秒, 如果没有设置超时则返回 None。
s.fileno()
: 返回套接字的文件描述。
s.setblocking(flag)
: 如果 flag 为 0, 则将套接字设置为非阻塞模式, 否则将套接字设置为阻塞模式(默认值)。非阻塞模式下, 如果调用 recv() 没有发现任何数据, 或 send() 调用无法立即发送数据, 那么将引起 socket.error 异常。
s.makefile()
: 创建一个与该套接字相关的文件。
服务端例子
网络编程包含两个部分: 服务器端和客户端。
服务端例子:
# tcp_server.py
import socket
HOST = '127.0.0.1'
PORT = 5000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建套接字
s.bind((HOST, PORT)) # 绑定套接字到本地 IP 与端口
s.listen(5) # 监听连接
print(f'Server start at: {HOST}:{PORT}')
while True: # 服务器可以无限循环地接收内容
conn, addr = s.accept() # 接受客户端连接
print(f'Connectd by {addr}')
while True: # 通信循环, 因为一次发送的内容可能很大
data = conn.recv(1024).decode('utf-8') # 接收 1024 字节的内容
print(data)
# 给客户端发送数据, send() 方法接收 byte 类型的数据
conn.send(bytes(f'Server received {data}', encoding='utf-8'))
conn.close() # 关闭客户端套接字
s.close() # 关闭服务器套接字
客户端例子
# tcp_client.py
import socket
HOST = '127.0.0.1'
PORT = 5000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT)) # 尝试连接到服务器
while True:
cmd = input('Input your msg:')
s.send(bytes(cmd, encoding='utf-8'))
data = s.recv(1024).decode('utf-8')
print(data)
启动服务端和客户端
启动服务端:
> python tcp_server.py
Server start at: 127.0.0.1:5000
Connectd by ('127.0.0.1', 9474) # 客户端启动后请求的端口是 9474
你好 # 客户端发来的内容
哈哈哈 # 客户端发来的内容
启动客户端:
> python tcp_client.py
Input your msg:你好 # 输出要发送的内容
Server received 你好 # 服务端返回的内容
Input your msg:哈哈哈
Server received 哈哈哈
Input your msg:
# 断开连接按 ctrl+c
网友评论