python用socket发送http请求

平时我们使用浏览器浏览web资源,写爬虫的时候,我们会使用封装好的库,比如requests,或者使用爬虫框架。工欲善其事必先利其器,顶层封装好的东西,是为了我们使用着方便,节省开发时间,尽管各种http库功能强大,但学习底层的技术仍然有着实践意义,只有了解底层,才能真正理解顶层的封装和设计,遇到那些艰难的问题时,才会有思路,有方案。

1. 用socket发送http请求

浏览器也好,爬虫框架也罢,在最底层,都是在使用socket发送http请求,然后接收服务端返回的数据,浏览器会对返回的数据进行渲染,最终呈现在我们眼前,爬虫框架相比于浏览器,只是少了一个渲染的过程。

用socket发送http请求,首先要建立一个TCP socket,然后连接到服务端socket,http请求的3次握手,本质上是TCP socket建立连接的3次握手。连接建立好了以后,就要发送数据了,这里的数据,可不是随意发送的,而是要遵照http协议,下面的代码,演示了socket发送http请求的过程

import socket


url = 'www.zhangdongshengtech.com'
port = 80

# 创建TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务端
sock.connect((url, port))
# 创建请求消息头
request_url = 'GET /article-types/6/ HTTP/1.1\r\nHost: www.zhangdongshengtech.com\r\nConnection: close\r\n\r\n'
print(request_url)
# 发送请求
sock.send(request_url.encode())
response = b''
# 接收返回的数据
rec = sock.recv(1024)
while rec:
    response += rec
    rec = sock.recv(1024)
print(response.decode())

代码非常简单,不做过多解释,我们重点要了解的是http协议,request_url的内容是

GET /article-types/6/ HTTP/1.1
Host: www.zhangdongshengtech.com
Connection: close

请求的消息头,每一行都有各自的作用,在消息头和消息体之间,有两次换行,由于我们发送的是GET请求,没有消息体,因此两个换行后就结束了。

这3行,每一行都至关重要。

第一行的内容,包含了三个重要信息

  1. GET 指明本次请求所使用的method,这是一次GET请求
  2. /article-types/6/ 指明了要请求的资源地址
  3. HTTP/1.1 指明http协议的版本,更早以前是1.0,现在大家都在用1.1

第二行的内容,指明了host,一台服务器上,也许不只是部署了一个web服务,而是多个,他们都是80端口,url = 'www.zhangdongshengtech.com' 只是告诉socket去哪里建立连接,这仅仅是个域名而已,程序根据域名找到IP地址,如果服务端部署多个服务,为了服务端区分一个请求是指向哪个服务,客户端需要在请求头中指明host,服务端会根据这个host来做请求的转发,这就是常说的nginx反向代理。

第三行,定义了Connection的值是close,如果不定义,默认是keep-alive, 如果是keep-alive,那么服务端在返回数据后不会断开连接,而是允许客户端继续使用这个连接发送请求,我故意设置成close,目的就是让服务端主动断开,这样,当程序在使用while循环时,接收完所有的数据后,sock.recv(1024) 返回的就是None,这样,就可以停止程序了。如果是keep-alive,无法通过连接断开来判断数据是否已经全部接收,那么就只能通过返回数据的消息头来获取数据的长度,进而决定本次请求返回的数据到哪里结束。

2. 返回的消息体

程序输出了服务端返回的数据,由于数据量很大,我们只截取消息头的部分进行讲解,消息体只是网页源码而已,没什么可说的。

HTTP/1.1 200 OK
Server: openresty/1.11.2.1
Date: Sun, 05 May 2019 03:11:05 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 29492
Connection: close
Set-Cookie: session=eyJjc3JmX3Rva2VuIjp7IiBiIjoiTn
prd1pqZGhaamd6T1dObFlUQTRZVFJqTkRJeU9USmtNalU0TldOaU1UQXdNamsxTkdSaVpRPT0ifX0.D6_lyQ.
4EqkK8taszUkPtMsol-8pzF_LQM; HttpOnly; Path=/

<!DOCTYPE html>
<html lang="en">
<head>

返回的数据中,消息头和消息体之间,也是两个换行。

第一行,HTTP/1.1 200 OK 指明了http协议的版本,已经本次请求返回的状态码,200表示成功响应,比较常见的还有404,500,302,这些状态码的含义,你可以自己百度一下。

第二行开始的消息头内容中,比较重要的是Content-Length,它的值是29492,这表明,消息体的长度是29492,如果Connection的值是keep-alive,客户端就得根据这个值来读取消息体。

请求的消息体和服务端响应的消息体中,除了第一行外,其他的长的很像字典形式的key-value对,叫首部,本文只涉及到个别几个首部,其他首部及其含义,你可以自行百度。

3. 从消息头解析出content_length

增加一点难度,从消息头里获取content_length,在获取返回数据时,当消息体的长度满足要求时,停止获取数据,并关闭连接

import socket


url = 'www.zhangdongshengtech.com'
port = 80
# 创建TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务端
sock.connect((url, port))
# 创建请求消息头
request_url = 'GET /article-types/6/ HTTP/1.1\r\nHost: www.zhangdongshengtech.com\r\n\r\n'
print(request_url)
# 发送请求
sock.send(request_url.encode())
body = ''
# 接收返回的数据
rec = sock.recv(1024)


index = rec.find(b'\r\n\r\n')       # 找到消息头与消息体分割的地方
head = rec[:index]
body = rec[index+4:]

# 获取Content-Length
headers = head.split(b'\r\n')
for header in headers:
    if header.startswith(b'Content-Length'):
        content_length = int(header.split(b' ')[1])

length = len(body)

while length < content_length:
    rec = sock.recv(1024)
    length += len(rec)
    body += rec

sock.close()
print(length)
print(head.decode())
print(body.decode())

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案