网络分层模型
在第一篇网络编程基础篇章中我们了解到,要想创建一个网络应用程序需要创建套接字,套接字分为面向连接和无连接的。而面向连接的基于TCP协议,无连接的基于UDP协议,可以看到这两个协议都位于传输层。尽管IP协议位于网络层,而且IP协议是所有互联网协议的基础,但事实上,很少会直接基于网络层进行应用程序编程。
Python提供了socket等模块针对传输层协议进行编程。
理解socket
socket是应用层与TCP/IP协议族通信的中间抽象层,它是一组接口。在设计模式中,它把复杂的TCP/IP协议隐藏在Socket接口后面,以此简化使用者的编程难度,使用者只需让socket去组织数据,以符合指定的协议。
socket最初是为同一主机上的应用程序所创建,使之能够实现同一主机上的多个进程间的通信,但后来逐渐成为实现不同主机间的进程通信的一种主要方式。
通过socket模块,我们可以很容易地找到一个应用程序并与之通信。
socket模块
要想创建套接字,可以使用socket模块下socket()函数,socket()函数的一般语法如下:
socket.socket(socket_family,socket_type,protocol=0)
其中,socket_family是AF_UNIT或者AF_INET,socket_type可以选择SOCK_STREAM或者SOCK_DGRAM,这正好对应我们此前提到的相关知识。至于protocol通常省略,默认为0。
我们使用如下语句创建TCP/IP套接字:
import socket
tcpSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
使用如下语句创建UDP/IP套接字:
import socket
tcpSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
创建一个套接字后返回一个套接字对象,套接字对象内置许多方法,这里不再全部列出,在接下来提到哪个方法时再做介绍,如果想全部了解的也可以查阅官方文档。不过官方的都是英文的如果嫌查阅不方便,也可以参阅这里:
https://www.runoob.com/python/python-socket.html
TCP socket网络编程
根据socket_type参数的不同,socket编程又可分为TCP socket编程和UDP socket编程,因为TCP socket是基于Client-Server的编程模型,即客户端-服务器架构模型,因此对TCP socket网络编程也分为客户端编程和服务器编程两部分。
客户端编程
还记得上一篇说过的关于客户端-服务器编程模型的一般流程么?接下来用实际的代码操作一步步还原它们。
1.创建TCP连接
在TCP 客户端编程中,创建一个TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
举例说明,当我们用浏览器访问百度时,浏览器就是一个客户端,浏览器会主动向百度服务器发起连接,之后如果没有异常就意味着建立了一个TCP连接。
建立成功后,就可以开始通信了。
为此,我们将先创建一个基于TCP连接的Socket:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('www.baidu.com',80))
在创建一个socket_family时,AF_INET指定使用IPv4协议,当然你也可以使用AF_INET6来指定使用最新的IPv6协议。SOCK_STREAM指定使用面向流的TCP协议,因此SOCK_STREAM也叫流式套接字。
之后我们使用客户端套接字的方法connect()主动向服务器发起连接,当然,要想连接到服务器,必须知道它的IP地址和端口号,这也是上一篇已经涉及过的知识,注意connect的参数必须是一个元组的格式,元组内第一个参数是IP地址,第二个是端口号。
但是你可能注意到,这里connect参数元组内的第一个参数是一个网址,并不是IP地址,这是因为域名网址可以自动转换到IP地址,这也省了我们的功夫,另外对于端口号,这是一个固定的值,服务器提供什么样的服务,它就有与之对应的端口号,因为要使服务器提供访问网页的功能时必须指定端口号固定在80,因此我们使用80作为端口号,80端口也正是Web服务的标准端口。
有效的端口号范围在0~65535之间,但端口号小于1024的是Internet标准服务的端口,端口号大于1024的可以任意使用。
如果有时你确实想要直接获得远程主机的IP地址,那么Python也提供有相应的函数,即socket.gethostbyname(),该方法用于获取远程主机的IP地址。它要求传入一个域名作为参数,如果获取失败,将抛出socket.gaierror异常。
示例如下:
import socket,sys
host = 'www.baidu.com'
port = 80
try:
get_ip = socket.gethostbyname(host)
except socket.gaierror:
print("Get IP Error!")
sys.exit()
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((get_ip,port))
另外注意,gethostbyname()方法只返回主机名的IPv4格式,不支持IPv6的域名解析。与之类似的还有gethostbyname_ex()方法,它是gethostbyname()的加强版,但同样不支持IPv6域名解析。
2.发送请求
TCP连接创建完成后,我们就可以发送请求了,发送一个TCP消息需要用到send()方法,我们可以通过指定send()参数向服务器提出命令请求,比如字符串GET / HTTP/1.1\r\n\r\n,这是一个HTTP请求网页内容的命令。
示例如下:
s.send(b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n')
添加异常处理:
mes = b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n'
try:
s.send(mes)
except socket.error:
print('Send Error!')
sys.exit()
注意:send()函数的参数只接受bytes类型,这点在网络编程中很普遍,因为服务器和浏览器只认bytes类型的数据,因此在send()函数的字符串前需加前缀修饰符 ,表示后面的字符串是bytes类型。
另外,TCP创建的是双向通道,即双方都能同时给对方发数据,但是具体谁先发谁后发,怎么协调则根据具体的协议来决定。例如HTTP协议规定客户端必须先发请求给服务器,服务器收到后才能给客户端发送数据。
3.接收数据
创建连接了,也发送请求了,接下来需要接收数据,套接字对象的recv()方法用于接收数据。
buffer = []
while True:
d = s.recv(1024) #每次最多接收1024字节,即1k的数据
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
在示例中,通过一个while True循环不断接收数据,直到返回空数据,此时接收完毕,至此一个通信就差不多完成了。
当然还有最后一步不要忘了。
4.关闭Socket
接收完数据后,我们调用close()方法关闭Socket,至此才算彻底结束了一次TCP通信。
即:
s.close()
5.总结
客户端编程体现了我们上一篇提到的客户端-服务器架构模型中客户端的一般执行流程,即:
创建Socket → 建立TCP连接 → 发送请求 → 接收数据 → 关闭Socket
完整代码:
import socket,sys
host = 'www.baidu.com'
port = 80
buffer = []
mes = b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n'
try:
get_IP = socket.gethostbyname(host)
except socket.gaierror:
print("Get IP Error!")
sys.exit()
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((get_IP,port))
try:
s.send(mes)
except socket.error:
print('Send Error!')
sys.exit()
while True:
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
s.close()
之后我们可以把接收到的data数据分离一下,因为接受到的数据包含百度的HTTP头和网页本身,我们把HTTP头打印出来,网页内容保存在文件内:
示例:
header,html = data.split(b'\r\n\r\n',1)
print(header.decode('utf-8'))
with open('index.html','wb') as f:
f.write(html)
至此我们的客户端编程就到此结束了,了解这个过程是很有意义的,因为当你打开百度的时候,浏览器做的工作也大抵就是如此。浏览器就是一个客户端,或者准确来说是一个HTTP客户端。
服务器编程
1.绑定端口
要想创建一个服务器首先要为它绑定端口,因为服务器本身也需要占用一个端口来监听客户端的请求。一旦端口绑定完成,服务器就可以启动监听器,从而进入无限等待客户端请求的循环状态。
import socket
host = socket.gethostname() #gethostname()方法获得本地主机号
port = 22223 #端口号大于1024的可以随意绑定,注意只有具有管理员权限才能绑定小于1024的端口号
ADDR = (host,port)
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(ADDR)
bind()方法将地址元组(主机名,端口号)绑定到套接字上。
这里我们将服务器绑定到本地地址,这意味着外部的计算机将无法连接进来,同时我们用gethostname()方法获取本机地址,你也可以用127.0.0.1来指定主机名,127.0.0.1是一个特殊的IP地址,它表示本机地址。同样特殊的还有0.0.0.0,它表示所有的网络地址。
2.启动TCP监听器
绑定地址后,我们只需要再启动监听器就可以使服务器进入永不休止的工作状态。事实上,服务器在理想状态下应该是永不休止的,但显然实际中总有出入,但这里我们暂且不去考虑这些问题。
s.listen(5)
套接字对象的listen()方法设置并启动一个监听器,它的参数指定了等待服务器连接的最大客户端数量。
3.等待请求/无限循环
开启监听器之后,服务器将进入无限循环的工作状态,套接字对象的accept()方法使服务器被动接受TCP客户端的连接,一直等待直到连接到达。
也就是说,调用accept()方法之后,就开启了一个单线程服务器,在客户端连接到服务器之前该方法将处于阻塞状态。一旦服务器接受了一个连接,就会返回一个独立的客户端套接字(临时套接字),该套接字用来与即将到来的信息进行交换,这样可以空出主线,即原始服务器套接字,以便它能继续等待新的客户端连接请求。
临时套接字将完成与连接来的客户端进行通信的全部工作,但注意这仍然是一个单线程的程序,临时套接字在处理通信请求的过程中,原套接字无法接受其他客户端的连接。
代码:
while True:
childsock,addr = s.accept() #accept()方法返回一个临时套接字,和其地址
while True:
data = childsock.recv(1024)
if not data:
break
childsock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
childsock.close()
s.close()
注意最后一行的代码,s.close()意味着关闭主TCP服务器,但是一个理想的TCP服务器永远不会被关闭,即最后一行的代码永远不会执行,它只是用来起到一个提醒作用:一是服务器不能被关闭,二是就算我们想要关闭,s.close()就是一个可行的方法。
4.服务器测试
先看一下创建服务器的完整代码:
import socket
host = socket.gethostname()
port = 1025
ADDR = (host,port)
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(ADDR)
s.listen(5)
while True:
childsock,addr = s.accept()
childsock.send(b'Welcome!')
while True:
data = childsock.recv(1024)
if not data:
break
childsock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
childsock.close()
接下来来测试它,要测试这个服务器,还需要一个客户端程序:
import socket
host = socket.gethostname()
port = 1025
ADDR = (host,port) #地址要与服务端相同,不然就找不到
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((ADDR))
print(s.recv(1024).decode('utf-8'))
for data in [b'Alice',b'Tom',b'Jerry']:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'')
s.close()
要测试服务器,我们先运行服务器程序,之后运行客户端程序,注意两个程序都要运行,之后会看到客户端程序运行窗口成功输出:
Welcome!
Hello,Alice!
Hello,Tom!
Hello,Jerry!
另外注意服务器程序会一直运行下去,需要Crtl+C退出。
5.多线程服务器
除此之外,如果想让服务器进行多线程服务,则需要借助于thread模块。
示例:
import thread,socket
def tcplink(sock):
while True:
data = sock.recv(1024)
if not data:
break
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connect from %s close' % (addr))
while True:
childsock,addr = s.accept()
childsock.send(b'Welcome!')
t = threading.Thread(target=tcplink,args=(childsock))
t.start()
6.总结
回顾一下创建一个TCP服务器的一般流程:
创建Socket → 绑定地址 → 启动TCP监听器 → 等待客户端连接 → 处理客户端请求 → 无限循环
参考:
https://www.liaoxuefeng.com/wiki/1016959663602400/1017788916649408
https://www.cnblogs.com/yzh2857/p/9642012.html
https://www.runoob.com/python/python-socket.html
《Python核心编程》