自计算机诞生以来,计算机网络逐渐从单机模型发展成了网络互联模型
最初只是远程终端连接,终端(键盘和显示器)分布在各地然后与主机相连,用户通过终端来与远程主机进行交互,终端只能与主机通信
再到多个主机之间互联,几台固定的计算机相连在一起形成计算机网络,这种网络一般是私有的(局域网)
随着时代的发展,人们开始尝试在私有网络上搭建更大的私有网络,逐渐又发展演变为互联网,现在我们每个人几乎都能够享有互联网带来的便利
计算机网络就是把各个计算机相互连接,让网络中的计算机能够互相通信传递数据,而网络编程就是实现程序如何在两台计算机之间进行通信
既然网络编程能让两个程序进行通信,在网络中,程序是通过什么来找到另一个程序的?
套接字(socket),表示方式为点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开(IP地址:端口号)
有了套接字,不同计算机之间的程序就可以进行双向通信
一般两个程序间的通信对应的软件开发架构有两种——C/S 架构和 B/S 架构
C/S 架构
Client 与 Server
客户端与服务器端架构,这种架构是从用户层面(也可以是物理层面)来划分的
这里的客户端一般泛指客户端应用程序,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大
例如:QQ、微信、网盘
B/S 架构
Browser 与 Server
浏览器端与服务器端架构,这种架构也是从用户层面来划分的
浏览器,其实也是一种 Client,只是这个 Client 不需要大家去安装什么应用程序,只需在浏览器上通过 HTTP 请求 server 相关的资源(网页资源)
例如:百度、各种应用的网页版
socket
Python 提供了两个级别的网络服务模块:
socket
- 低级别的网络服务模块,提供了标准的 BSD Sockets API,可以访问底层操作系统 Socket 接口的全部方法
socketserver
- 高级别的网络服务模块,它提供了服务器中心类,可以简化网络服务器的开发
今天我们主要介绍 socket 模块
先导入 socket 模块
import socket
#语法
socket.socket([family[, type[, proto]]])
- family:套接字家族;可以是 AF_UNIX 或者 AF_INET
- type:套接字类型;可以根据是面向连接(TCP)的还是非连接(UDP)分为 SOCK_STREAM 或 SOCK_DGRAM
- protocol:一般不填默认为 0
server 端 socket 函数
函数 | 描述 |
---|---|
s.bind() | 绑定地址(host,port)到套接字<br/>在 AF_INET 中,以元组(host,port)的形式表示地址 |
s.listen() | 开始 TCP 监听<br/>backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量<br/>该值至少为 1,大部分应用程序设为 5 就可以了 |
s.accept() | 被动接受TCP客户端连接,(阻塞式)等待连接的到来 |
client 端 socket
函数 | 描述 |
---|---|
s.connect() | 主动初始化 TCP 服务器连接<br/>一般 address 的格式为元组(hostname,port),如果连接出错返回socket.error 错误。 |
s.connect_ex() | connect()函数的扩展版本<br/>出错时返回出错码,而不是抛出异常 |
公共用途的 socket
函数 | 描述 |
---|---|
s.recv() | 接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量<br/>flag 提供有关消息的其他信息,通常可以忽略。 |
s.send() | 发送 TCP 数据,将 string 中的数据发送到连接的套接字<br/>返回值是要发送的字节数量,该数量可能小于 string 的字节大小。 |
s.sendall() | 完整发送 TCP 数据<br/>将 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据<br/>成功返回 None,失败则抛出异常。 |
s.recvfrom() | 接收 UDP 数据,与 recv() 类似,但返回值是(data,address)<br/>其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址。 |
s.sendto() | 发送 UDP 数据,将数据发送到套接字,address 是形式为(ip,port)的元组,指定远程地址。<br/>返回值是发送的字节数。 |
s.close() | 关闭套接字 |
s.getpeername() | 返回连接套接字的远程地址<br/>返回值通常是元组(ip,port)。 |
s.getsockname() | 返回套接字自己的地址<br/>通常是一个元组(ip,port) |
s.setsockopt(level,optname,value) | 设置给定套接字选项的值 |
s.getsockopt(level,optname[.buflen]) | 返回套接字选项的值 |
s.settimeout(timeout) | 设置套接字操作的超时期,timeout 是一个浮点数,单位是秒。<br/>值为None表示没有超时期。一般超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect()) |
s.gettimeout() | 返回当前超时期的值,单位是秒<br/>如果没有设置超时期,则返回None。 |
s.fileno() | 返回套接字的文件描述符 |
s.setblocking(flag) | 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)<br/>非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。 |
s.makefile() | 创建一个与该套接字相关连的文件 |
TCP 编程
如上图左边所示,server 进程首先要绑定一个端口并监听来自 client 的连接,如果某个 client 的连接过来了,server 就与 该 client 建立 socket 连接
所以 server 会打开端口(比如 80)监听,每来一个 client ,就创建该 socket 连接
考虑到会有大量的 client 与 server 进行连接,server要能够区分一个 socket 连接对应哪个 client
一个 socket 由四个元素组成:
- server 地址(目标地址)
- client 地址(源地址)
- server 端口(目标端口)
- client 端口(源端口)
除此之外,server 还需要同时响应多个 client 的请求,所以每个连接都需要一个新的进程或者新的线程来处理,否则 server 一次就只能处理一个 client 的请求了
我们来编写一个简单的 server 程序,它接受 client 连接,把 client 发过来的数据加上 hello 再返回给 client
首先创建一个基于 ipv4 和 TCP 协议的 socket 对象
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
然后我们绑定监听的地址和端口(可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0
绑定到所有的网络地址,还可以用127.0.0.1
绑定到本机地址)
这里我们是通过本机来实现 C/S 架构,所以绑定到 127.0.0.1 上,而且不要绑定端口号小于 1024 的端口(要有管理员权限才能绑定)
ps:端口复用
我们知道 TCP 关闭连接有一个四次挥手的过程,当 server 主动关闭连接时,会有一个TIME_WAIT(时间等待)状态,等待 2MSL(最长报文段寿命)后进入关闭状态
那么在这个 TIME_WAIT 状态下,端口还处于被别的进程绑定的状态之中,那么其他进程就会拿不到这个端口,产生报错
我们可以通过端口复用来解决这个问题
#提示:socket.setsockopt()方法要在 socket.bind()之前设置
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
level:设置哪个级别的 socket, socket.SOL_SOCKET 表示当前socket
option:设置什么内容(权限) socket.SO_REUSEADDR 端口复用
value:True:表示复用,False,表示不复用
#绑定本地9999端口
s.bind(('127.0.0.1', 9999))
接着开始监听端口,参数为指定等待连接的最大数量
#监听的连接数最多为5
s.listen(5)
print('Waiting for connection...')
server 程序通过一个 while 循环来接受来自 client 的连接,accept()
会等待并返回一个 client 的连接
当有 client 来连接时,就创建一个线程来处理会话
while True:
sock, addr = s.accept()
t = threading.Thread(target=tcplink, args=(sock,addr))
t.start()
连接建立后,server 首先返回一条欢迎消息,然后等待 client 的数据,收到数据之后并加上 hello 再发送给 client
如果没有数据或者 client 发送了 exit 字符串就关闭连接
def tcplink(sock,addr):
print('Accept new connection from %s:%s...' % addr)
#返回欢迎信息
sock.send(b'hello!')
#当 client 发送数据时,返回 hello+数据给 client
while True:
data = sock.recv(1024)
time.sleep(1)
#如果 client 不发送数据或者发送 exit,退出连接
if not data or data.decode('utf-8') == 'exit':
break
else:
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)
接下来我们编写一个 client 程序
创建一个基于 ipv4 和 TCP 协议的 socket 对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
客户端要主动发起 TCP 连接,必须知道服务器的 IP 地址和端口号
#注意参数是一个tuple,包含地址和端口号
s.connect(('127.0.0.1', 9999))
TCP 连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定
例如,HTTP协议规定 client 必须先发请求给 server,server 收到后才发数据给 client
接收数据时,调用 recv(max) 方法,一次最多接收指定的字节数
mes = s.recv(1024)
#接收到的数据是 byte 格式,需要转码
print(mes.decode('utf-8'))
我们将数据发送给 server,并接收 server 返回的数据
for data in [b'Travis', b'EDISON', b'JOHN']:
s.send(data)
print(s.recv(1024).decode('utf-8'))
当通信完之后,发送 exit 给 server,然后调用 close() 方法关闭 Socket,这样,一次完整的网络通信就结束了
s.close()
UDP编程
TCP 是建立可靠连接,并且通信双方都可以以流(stream)的形式发送数据,相对于 TCP ,UDP 则是面向无连接的协议
使用 UDP 协议时,不需要建立连接,只需要知道对方的 IP 地址和端口号就可以直接发送数据包
虽然用 UDP 传输数据不可靠,但它的优点是和 TCP 比,速度快,对于不要求可靠到达的数据,就可以使用 UDP 协议
首先编写一个 server 程序
创建一个 socket 对象,SOCK_DGRAM 指定使用面向流的 UDP 协议
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
绑定端口 8888,也可以绑定 9999
服务器绑定 UDP 端口和 TCP 端口互不冲突,也就是说,UDP 的 9999 端口与 TCP 的 9999 端口可以各自绑定
s.bind(('127.0.0.1', 8888))
与 TCP 不同的是,UDP 不需要监听端口,而是直接接收来自 client 的数据
print('Bind UDP on 8888........')
while True:
#recvfrom 接收数据,返回(data,address)
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
#sendto 发送数据,发送形式是(data,address)
s.sendto(b'Hello, %s!' % data, addr)
#关闭套接字
s.close()
然后编写一个 client 程序
创建一个 socket 对象,SOCK_DGRAM 指定使用面向流的 UDP 协议
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client 使用 UDP 时,首先仍然创建基于 UDP 的 Socket,然后,不需要调用 connect(),直接通过 sendto() 给 server 发数据
for data in [b'Edison', b'Kanye', b'Kendrick']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据:
print(s.recv(1024).decode('utf-8'))
#关闭套接字
s.close()