由下图可理解:Socket是应用层与TCP/IP协议族通信的中间软件抽象层。
复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
由下图可理解服务器和客户端通信的流程:
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
网络中进程之间通信:
首要解决的问题是如何唯一标识一个进程。
TCP/IP协议族已经帮我们解决了这个问题,
网络层的“ip地址”可以唯一标识网络中的主机,
而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。
这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
一个socket包含四个地址信息: 两台计算机的IP地址和两个进程所使用的端口(port)。IP地址用于定位计算机,而port用于定位进程。
在互联网上,我们可以让某台计算机作为服务器。
服务器开放自己的端口,被动等待其他计算机连接。
当其他计算机作为客户,主动使用socket连接到服务器的时候,服务器就开始为客户提供服务。
在Python中,我们使用标准库中的socket包来进行底层的socket编程。
服务器端:使用bind()方法来赋予socket以固定的地址和端口,并使用listen()方法来被动的监听该端口。当有客户尝试用connect()方法连接的时候,服务器使用accept()接受连接,从而建立一个连接的socket:
import socket
HOST = ''
PORT = 8000
reply = 'Yes'
'''
socket.socket()创建一个socket对象,
并说明socket使用的是IPv4(AF_INET,IP version 4)
和TCP协议(SOCK_STREAM)。
'''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(3)
conn, addr = s.accept()
request = conn.recv(1024)
print('request is:', request.decode())
print('Connected by:', addr)
conn.sendall(reply.encode())
conn.close()
客户端:主动使用connect()方法来搜索服务器端的IP地址和端口,以便客户可以找到服务器,并建立连接:
import socket
HOST = '127.0.0.1'
PORT = 8000
request = 'can you hear me?'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
'''
TypeError: a bytes-like object is required, not 'str'
解决办法:
str→bytes:encode()方法。str通过encode()方法可以转换为bytes。
bytes→str: decode()方法。bytes通过decode()方法可以转换为str。
'''
s.sendall(request.encode())
reply = s.recv(1024)
# send 函数的参数和 recv 函数的返回值都是 bytes 类型
print('reply is:', reply.decode())
s.close()
服务器端运行结果:
request is: can you hear me?
Connected by: ('127.0.0.1', 1489)
客户端运行结果:
reply is: 'Yes'
我本机的ipv4地址是10.1.173.8。我把服务器和客户端都放到一个电脑上。
我测试了一下,在客户端代码中,我设置HOST = ‘10.1.173.8’。
然后,运行客户端和服务器代码,产生和HOST = '127.0.0.1’同样结果。
也就是说,客户端的HOST,为客户端IP想要connect的IP。
为了证明这一想法,我把客户端设置成HOST = ‘10.1.172.1’
再次运行,出现报错:
s.connect((HOST, PORT))
TimeoutError: [WinError 10060] 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。
可见,如果没有两台计算机做实验,可以将客户端IP想要connect的IP改为"127.0.0.1",这是个特殊的IP地址,用来连接当地主机。
而且,如果知道服务器的IP,那么也就可以在客户端直接设置HOST = 服务器的ip
下面我测试一下端口号PORT:
服务器端和客户端 端口号都相同时候,可以建立连接。而且端口的数不会影响连接,都是8000和都是8080,都能连上。
端口号不同就连接不上。
服务端socket函数 | 描述 |
---|---|
s.bind(address) | 将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址. |
s.listen(backlog) | 开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。 |
s.accept() | 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。 |
客户端socket函数 | 描述 |
---|---|
s.connect() | 主动初始化TCP服务器连接。一般address的格式为元组(主机/ip,port),如果连接出错,返回socket.error错误。 |
s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共socket函数 | 描述 |
---|---|
s.recv() | socket.recv(bufsize[, flags]),从套接字接收数据。返回值是表示接收到的数据的bytes对象。bufsize指定一次接收的最大数据量。一般为1024开始 |
s.send() | 发送数据,将数据发送到socket套接字。(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) |
s.sendall() | 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) |
s.close() | 关闭socket 套接字 |
TCP服务端:
1 创建套接字,绑定套接字到本地IP与端口
# socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.bind()
2 开始监听连接 #s.listen()
3 进入循环,不断接受客户端的连接请求 #s.accept()
4 然后接收传来的数据,并发送给对方数据 #s.recv() , s.sendall()
5 传输完毕后,关闭套接字 #s.close()
TCP客户端:
1 创建套接字,连接远端地址
# socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.connect()
2 连接后发送数据和接收数据 # s.sendall(), s.recv()
3 传输完毕后,关闭套接字 #s.close()
上面的例子中,使用TCP socket来为两台远程计算机建立连接。
然而,socket传输自由度太高,从而带来很多安全和兼容的问题。
往往利用一些应用层的协议(比如HTTP协议)来规定socket使用规则,以及所传输信息的格式。
HTTP协议利用请求-回应(request-response)的方式来使用TCP socket。
客户端向服务器发一段文本作为request,服务器端在接收到request之后,向客户端发送一段文本作为response。
在完成了这样一次request-response交易之后,TCP socket被废弃。
下次的request将建立新的socket。
request和response本质上说是两个文本,只是HTTP协议对这两个文本都有一定的格式要求。
import socket
HOST = ''
PORT = 8000
text_content = b'''HTTP/1.x 200 OK
Content-Type: text/html
WOW
Wow, Python Server
'''
f = open('test.jpg', 'rb')
pic_content = b'''
HTTP/1.x 200 OK
Content-Type: image/jpg
'''
pic_content = pic_content+f.read()
f.close()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
while True:
s.listen(3)
conn, addr = s.accept()
request = conn.recv(1024)
method = request.decode().split(' ')[0]
src = request.decode().split(' ')[1]
if method == 'GET':
if src == '/test.jpg':
content = pic_content
else:
content = text_content
print('Connected by', addr)
print('Request is:', request)
conn.sendall(content)
conn.close()
为了配合上面的服务器程序,在放置Python程序的文件夹里,保存了一个test.jpg图片文件。
在终端运行上面的Python程序,作为服务器端。
再打开一个浏览器作为客户端。
在浏览器的地址栏输入:127.0.0.1:8000,也可以用另一台电脑,并输入服务器的IP地址。
可以看到:
如我们上面所看到的,服务器会根据request向客户传输两条信息text_content和pic_content中的一条,作为response文本。
整个response分为**起始行(start line), 头信息(head)和主体(body)**三部分。
起始行就是第一行:
HTTP/1.x 200 OK
它实际上又由空格分为三个片段,HTTP/1.x表示所使用的HTTP版本,200表示状态(status code),200是HTTP协议规定的,表示服务器正常接收并处理请求,OK是供人来阅读的status code。
头信息跟随起始行,它和主体之间有一个空行。这里的text_content或者pic_content都只有一行的头信息,text_content的头信息
Content-Type: text/html
表示主体信息的类型为html文本
而pic_content的头信息
Content-Type: image/jpg
说明主体的类型为jpg图片(image/jpg)。
主体信息为html或者jpg文件的内容。
(注意,对于jpg文件,我们使用’rb’模式打开,是为了与windows兼容。因为在windows下,jpg被认为是二进制(binary)文件,在UNIX系统下,则不需要区分文本文件和二进制文件。)
用浏览器作为客户端。
request由客户端程序发给服务器。
尽管request也可以像response那样分为三部分,request的格式与response的格式并不相同。
request由客户发送给服务器,比如下面是一个request:
GET /test.jpg HTTP/1.x
Accept: text/*
起始行可以分为三部分,第一部分为请求方法(request method),第二部分是URL,第三部分为HTTP版本。
request method可以有GET, PUT, POST, DELETE, HEAD。最常用的为GET和POST。
GET是请求服务器发送资源给客户,
POST是请求服务器接收客户送来的数据。
当我们打开一个网页时,我们通常是使用GET方法;
当我们填写表格并提交时,我们通常使用POST方法。
第二部分为URL,它通常指向一个资源(服务器上的资源或者其它地方的资源)。
像现在这样,就是指向当前服务器的当前目录的test.jpg。
按照HTTP协议的规定,服务器需要根据请求执行一定的操作。
正如我们在服务器程序中看到的,我们的Python程序先检查了request的方法,随后根据URL的不同,来生成不同的response(text_content或者pic_content)。
随后,这个response被发送回给客户端。
在服务器终端,可以看到浏览器发出的第一个请求:
Request is: b’GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n
服务器根据这个请求,发送给浏览器text_content的内容
浏览器接收到text_content之后,发现正文的html文本中有,知道需要获得text.jpg文件来补充为图片,立即发出了第二个请求:
Request is: b’GET /test.jpg HTTP/1.1\r\nHost: 127.0.0.1:8000
服务器的Python程序分析过起始行之后,发现/test.jpg符合if条件,所以将pic_content发送给客户
Connected by ('127.0.0.1', 5186)
Request is: b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nsec-ch-ua: "Chromium";v="94", "Microsoft Edge";v="94", ";Not A Brand";v="99"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: "Windows"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n\r\n'
Connected by ('127.0.0.1', 8177)
Request is: b'GET /test.jpg HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nsec-ch-ua: "Chromium";v="94", "Microsoft Edge";v="94", ";Not A Brand";v="99"\r\nsec-ch-ua-mobile: ?0\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50\r\nsec-ch-ua-platform: "Windows"\r\nAccept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nSec-Fetch-Site: same-origin\r\nSec-Fetch-Mode: no-cors\r\nSec-Fetch-Dest: image\r\nReferer: http://127.0.0.1:8000/\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n\r\n'
Connected by ('127.0.0.1', 6015)
Request is: b'GET /favicon.ico HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nsec-ch-ua: "Chromium";v="94", "Microsoft Edge";v="94", ";Not A Brand";v="99"\r\nsec-ch-ua-mobile: ?0\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50\r\nsec-ch-ua-platform: "Windows"\r\nAccept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nSec-Fetch-Site: same-origin\r\nSec-Fetch-Mode: no-cors\r\nSec-Fetch-Dest: image\r\nReferer: http://127.0.0.1:8000/\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n\r\n'
第三个请求 是因为浏览器默认都会去请求favicon.ico图标
favicon.ico 图标用于收藏夹图标和浏览器标签上的显示
上面的服务器程序中,用while循环来让服务器一直工作下去。实际上,还可以根据多线程的知识,将while循环中的内容改为多进程或者多线程工作。
服务器程序还不完善,还可以让Python程序调用Python的其他功能,以实现更复杂的功能。比如说制作一个时间服务器,让服务器向客户返回日期和时间。还可以使用Python自带的数据库,来实现一个完整的LAMP服务器。
socket包是比较底层的包。Python标准库中还有高层的包,比如SocketServer,SimpleHTTPServer,CGIHTTPServer,cgi。这些都包都是在帮助更容易的使用socket。如果已经了解了socket,那么这些包就很容易明白了。利用这些高层的包,可以写一个相当成熟的服务器。