纸上得来终觉浅,绝知此事要躬行。
自从互联网诞生以来,现在基本上所有的程序都是网络程序,很少有单机版的程序了。计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。用 Python 进行网络编程,就是在 Python 程序本身这个进程内,连接别的服务器进程的通信端口进行通信。
1. TCP/IP 协议
要编写一个计算机之间的网络通讯的程序,首先需要确定双方通信所使用的协议,之后才能够使用这种约定的协议进行数据的传输。
TCP/IP 协议的作用
TCP/IP
是用于因特网(Internet
)的通信协议。它是对计算机必须遵守的规则的描述,只有遵守这些规则,计算机之间才能进行通信。
TCP/IP 协议的分类
- IP - 网际协议
- TCP - 传输控制协议
- UDP - 用户数据报协议
网络编程重要的模块
协议 | 功能用处 | 端口号 | 对应模块模块 |
---|---|---|---|
HTTP |
网页访问 | 80 |
httplib 、urllib 、xmlrpclib |
NNTP |
阅读和张贴新闻文章 | 119 |
nntplib |
FTP |
文件传输 | 20 |
ftplib 、urllib |
SMTP |
发送邮件 | 25 |
smtplib |
POP3 |
接收邮件 | 110 |
poplib |
IMAP4 |
获取邮件 | 143 |
imaplib |
Telnet |
命令行 | 23 |
telnetlib |
Gopher |
信息查找 | 70 |
gopherlib 、urllib |
TCP 和 UDP 的区别
TCP
提供IP
环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠的数据包发送。- 通俗说,它是事先为所发送的数据开辟出连接好的通道,然后再进行数据发送。而
UDP
则不为 IP 提供可靠性、流控或差错恢复功能。一般来说,TCP 对应的是可靠性要求高的应用,而UDP
对应的则是可靠性要求低、传输经济的应用。
IPv4 地址的分类
- 公有地址
- A 类地址:
1-127
- B 类地址:
128-191
- C 类地址:
192-223
- D 类地址:
224-239
组播 - E 类地址:
240-254
- A 类地址:
- 私有地址
- A 类地址:
10.0.0.0/8
- B 类地址:
172.16.0.0/16 - 172.31.0.0/16
- C 类地址:
192.168.0.0/24 - 192.168.255.0/24
- A 类地址:
TCP 的三次握手
TCP 的四次挥手
2. 套接字
Socket
是进程间通信(IPC
)的一种实现,允许位于不同主机甚至同一主机上不同进程之间进行通信。socket
本质上是对TCP/IP
的封装,包含进行网络通信必需的五种信息:连接使用的协议,本地主机的 IP 地址,本地进程的协议端口,对方主机的 IP 地址,对方进程的协议端口。
- 套接字可以简单理解为:套接字 = IP 地址 + 端口号
- 进程间通信的实现不只有
socket
,还有信号量、共享内存、消息队列等方式
套接字的分类 - 根据传输使用的协议类型
- 流式套接字(
SOCK_STREAM
)TCP
通信协议,流式传输- 建立虚链路、双向通信、可靠地传递、面向连接、无边界
- 数据报套接字(
SOCK_DGRAM
)UDP
通信协议,数据报传输- 不可靠地传递、有边界、无连接
- 裸套接字(
SOCK_RAW
)- 不使用传输层协议,直接和底层进行数据传输,如
IP
- 在编程工作中,基本不会涉及,后续的三种套接字自行了解
- 不使用传输层协议,直接和底层进行数据传输,如
套接字的分类 - 根据套接字地址家族(Address Family
)
- IPv4 协议簇(
AF_INET
)- 不同主机之间进行通信时使用
- IPv6 协议簇(
AF_INET6
)- 不同主机之间进行通信时使用
- UNIX 协议簇(
AF_UNIX
)- 同一主机上不同进程之间通信时使用
- 不需要将数据向下传递,不占用
TCP
/UDP
协议栈,提升传输效率
套接字的端口号
- TCP 的端口号
0-65535
- 传输控制协议,面向连接的协议
- 通信前需要建立虚拟链路,结束后拆除链路
- UDP 的端口号
0-65535
- 用户数据报协议,无连接的协议
3. 函数模块
Socket
又称套接字,应用程序通常通过套接字网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。这里,我们将介绍Socket
套接字函数的使用方法。
创建套接字的语法
- 在
Python
中,网络编程主要用的是socket
模块
# socket_family: 主要包含AF_INET、AF_INET6、AF_UNIX
# socket_type: 主要包含SOCK_STREAM、SOCK_DGRAM、SOCK_RAW
# protocol: 用于指定创建套接字的协议,通常默认省略值为0
socket(socket_family, socket_type, protocol=0)
# 创建TCP套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Socket 模块的方法
- 服务器端套接字
套接字方法 | 解释说明 |
---|---|
s.bind(address) |
将地址(host, port) 绑定到套接字;在AF_INET 下以元组的形式表示地址 |
s.listen(backlog) |
开始监听TCP 传入的连接;backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量;该值至少为1 ,大部分应用程序设为5 就可以了 |
s.accept() |
被动接受TCP 客户端的连接并返回一个元组(conn, address) ;其中conn 是新的套接字对象,可以用来接收和发送数据;address 是连接客户端的地址 |
- 客户端套接字
套接字方法 | 解释说明 |
---|---|
s.connect(address) |
连接到address 服务器的套接字;一般address 的格式为元组(host, port) ;如果连接出错,则返回socket.error 错误 |
s.connect_ex(address) |
功能和connect() 函数一样;但成功的时候返回数字码0 ,而出错时返回出错码,而不是抛出异常 |
- 公共用途的套接字函数
套接字方法 | 解释说明 |
---|---|
s.recv(bufsize[, flag]) |
接收TCP 套接字的数据,数据以字符串形式返回;bufsize 指定要接收的最大数据量;flag 提供有关消息的其他信息,通常可以忽略 |
s.send(string[, flag]) |
发送TCP 套接字的数据,将string 中的数据发送到连接的套接字;返回值是要发送的字节数量,该数量可能小于string 的字节大小 |
s.sendall(string[, flag]) |
完整发送TCP 套接字的数据,将string 中的数据发送到连接的套接字;但在返回之前会尝试发送所有数据,成功返回None ,失败则抛出异常 |
s.recvfrom(bufsize[, flag]) |
接收UDP 套接字的数据,与recv() 类似,但返回值是元组(data, address) ;其中data 是包含接收数据的字符串;address 是发送数据的套接字地址 |
s.sendto(string[, flag], address) |
发送UDP 套接字的数据,将数据发送到指定的address 套接字;address 是形式为(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() |
创建一个与该套接字相关连的文件 |
4. TCP 编程
TCP
协议是面向连接的,会事先建立好连接,所以不需要指定地址
- 服务端示例代码
import socket
HOST = '127.0.0.1'
PORT = 8001
# 创建、绑定和监听套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(5)
print(f'Server start at: {HOST}:{PORT}')
while True:
# 接收客户端的连接
conn, addr = s.accept()
print(f'Connected by {addr}.')
# 通信循环
# 因为一次发送的内容可能很大,所以需要多次接收
while True:
data = conn.recv(1024)
print(f'[Receive client]: {data}')
# 服务端给客户端发送数据
# 使用send方法发送byte类型的数据,所有utf8格式需要手动转换一下
conn.send(bytes(f'Server received {data}.', 'utf-8'))
# 关闭客户端套接字
conn.close()
# 关闭服务端套接字
s.close()
$ python server.py
Server start at: 127.0.0.1:8001
Connected by ('127.0.0.1', 64138).
[Receive client]: b'hello'
[Receive client]: b'my name is escape.'
[Receive client]: b''
[Receive client]: b''
Traceback (most recent call last):
File "server.py", line 24, in <module>
conn.send(bytes(f'Server received {data}.', 'utf-8'))
BrokenPipeError: [Errno 32] Broken pipe
- 客户端示例代码
import socket
HOST = '127.0.0.1'
PORT = 8001
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
msg = input('Please input message: ')
s.send(bytes(msg, 'utf-8'))
data = s.recv(1024)
print(f'[Received Server]: {data}')
$ python client.py
Please input message: hello
[Received Server]: b"Server received b'hello'."
Please input message: my name is escape.
[Received Server]: b"Server received b'my name is escape.'."
Please input message: ^CTraceback (most recent call last):
File "client.py", line 10, in <module>
msg = input('Please input message: ')
KeyboardInterrupt
5. UDP 编程
而
UDP
是面向无连接的,所以每次发送都需要指定发送给谁
- 服务端示例代码
import socket
HOST = '127.0.0.1'
PORT = 8001
# 创建和绑定套接字,不需要监听连接
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind((HOST, PORT))
print(f'Server start at: {HOST}:{PORT}')
while True:
# 不需要接收连接,直接接收数据
data, addr = s.recvfrom(1024)
print(f'Connected by {addr}.')
print(f'[Receive client]: {data}')
# 服务端给客户端发送数据
s.sendto(bytes(f'{data}', 'utf-8'), addr)
# 关闭服务器套接字
s.close()
$ python runsocket.py
Server start at: 127.0.0.1:8001
Connected by ('127.0.0.1', 54007).
[Receive client]: b'test'
Connected by ('127.0.0.1', 54007).
[Receive client]: b'exit'
^CTraceback (most recent call last):
File "server.py", line 13, in <module>
data, addr = s.recvfrom(1024)
KeyboardInterrupt
- 客户端示例代码
import socket
HOST = '127.0.0.1'
PORT = 8001
# 不需要使用connect方法连接到服务器
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = input('Please input message: ')
s.sendto(bytes(msg, 'utf-8'), (HOST, PORT))
data = s.recvfrom(1024)
print(f'[Received Server]: {data}')
$ python runsocket.py
Please input message: test
[Received Server]: (b"b'test'", ('127.0.0.1', 8001))
Please input message: exit
[Received Server]: (b"b'exit'", ('127.0.0.1', 8001))
Please input message: ^CTraceback (most recent call last):
File "client.py", line 10, in <module>
msg = input('Please input message: ')
KeyboardInterrupt