通信、socket与http协议

通信

我们讲通信,是指一方对另一方信息交换的过程。信息交换需要两个条件,一个是信息,一个是载体。
为了通信,我们创造了许多,其中最重要的发明莫过于网络。有了网络我们就可以在不同的主机上传输信息,这其中通信的基础之一就是socket,当然它并不是最基础的,在计算机世界还有更底层的物理硬件层面的传输技术,但对写代码来说socket是网络通信最基础的技术了。

socket是什么

首先要明确,我们编码中用到的所有知识,技巧都来自于OS(操作系统),OS提供了什么样的api,代码层面就拥有什么能力。socket则在操作系统被创造时便赋予的能力,通过socket技术,我们就可以在不同的主机间通过网络传输数据了。

以LINUX为例,通过查看linux手册可以获取到socket接口的基本信息(在linux命令行中输入man socket),这里只是贴出定义,不要求看懂,可以直接看下方的总结

socket的定义

socket用于创建端到端的通信。定义socket需要三个参数。

1.domain参数指定了通信的域(域的理解就是,某种类型的抽像设备),设置domain将帮助socket选择通信的协议family(族?)
常见参数值的比如:

  • AF_UNIX 用于本地通信,例如同主机上的进程间通信
  • AF_INET 用于因特网通信,即不同主机间的进程通信

2.type参数指定通信语义
通常我们使用SOCK_STREAM,指定连接提供顺序的,可靠的,双向的,基于连接的字节流。 (其他参数在本文讨论的通信范畴里不会用到。)

3.protocol,该参数指定socket所使用的协议。通常,给定的协议族的socket类型只支持一种协议,这种情况下protocol可以为0.(即通常情况下,我们不会用到这个参数)

Python中的socket用法

1.建立socket server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8086))
s.listen()

conn, addr = s.accept()
with conn:
print('Connect by', addr)
d_list = []
while True:
data = conn.recv(1024)
if not data:
break

d_list.append(data)
conn.sendall(b"recv done.")

s.close()

使用socket要经过bind,listen,accept等流程,这是定义时已被决定的流程。

socket.AF_INET表示我们正在使用因特网,socket.SOCK_STREAM表明这个socket是因特网链接的一个端点,传输字节数据。
bind函数告诉操作系统内核将socket地址即(127.0.0.1,8085)与socket描述符s联系起来(在类unix系统中,一切设备都是文件,而文件的读写要通过描述符来进行)。
listen函数将描述符s转化为监听描述符,同时也告诉内核,这个描述符是被服务器使用,即当前是个server socket进程。
accept函数等待,客户端的连接请求是否到达bind时的socket地址。当有请求来时返回已连接描述符。当建立已连接描述符时,说明可以在服务端和客户端间传输数据了。

2.使用socket建立客户端并发送数据

1
2
3
4
5
6
7
8
9
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 8086))
s.send(b"Hello")

data = s.recv(1024)
print('Received:', repr(data))
s.close()

与服务端不同的是,客户端直接使用connect方法连接到远端的socket地址,客户端connect请求被服务端accept接收到。
当完成这个步骤后就可以使用send传输数据了。发送完毕后通过close()关闭socket连接,便完成了一次socket通信。

tips:前面提到的socket.SOCK_STREAM参数值是全双工(双向工作)的,因此,服务器也可以向客户端发送数据。

归纳推导

通过socket可以传输任意的文本输入了。
仅仅是发送文本还不够,我们会自然的想到,对文本约定一些格式,就可以传输更多类型。
比如:
传送dict类型的数据,我们可以约定字符串格式形如:“key1=value1,key2=value2……keyn=valuen”,
服务端收到时,反向解析一下,即可得到期望数据,解析函数如下:

1
2
3
4
5
6
7
8
9
def parse(data):
_dict = {}

data = data.decode('utf-8')
for pairs in data.split(','):
k, v = pairs.split('=')
_dict[k] = v

return _dict

所以此时我们可以通过socket client发送数据

1
s.send(b"name=test,pass=pppp")

而服务端解析

1
2
3
4
5
6
7
8
9
10
11
with conn:
print('Connect by', addr)
d_list = []
while True:
data = conn.recv(1024)
if not data:
break

print(parse(data))
d_list.append(data)
conn.sendall(b"recv done.")

得到结果

1
2
3
D:\env\Python38\python.exe D:/service/material/socket/server.py
Connect by ('127.0.0.1', 53360)
{'name': 'test', 'pass': 'pppp'}

由此,延展这一点,我们知道,可以指定越来越规范严格的约束,来传送更多的数据。
所以http来了,超文本传输协议。

HTTP协议

HTTP对socket传输的数据做了各种各样的约束,以至于可以传输媒体文件,超文本传输协议真不是浪的虚名。

因此,web服务器便是实现了http协议的socket服务器,浏览器则是以http约束数据格式发送数据的socket客户端。

我们可以构造自己的简单的web服务器,这需要一点点的http知识。

重要的两个概念:
Request: 客户端发送的数据
请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。

以这样的定义,我们构造一个GET请求

1
GET / HTTP/1.1 \r\nHOST 127.0.0.1:8085\r\n

Resonse: 客户端接收到的数据(服务端发送的数据)
响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

实现自己http服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#!/bin/python
# -*- coding: utf-8 -*-
import datetime
import socket


def parse(data):
"""

:param data:
:return:
"""
_dict = {}
if isinstance(data, bytes):
data = data.decode('utf-8')

pairs = data.split('\r\n')
request_line = pairs[0].split(' ')
_dict['method'] = request_line[0]
_dict['url'] = request_line[1]
_dict['http_version'] = request_line[2]
_dict['header'] = pairs[1]

return _dict


def handle(url):
"""

:param url:
:return:
"""
if url != "/":
status = 404
status_msg = 'Not Found'
content = 'oh no ! 404 happening.'
else:
status = 200
status_msg = "OK"
content = """<html><head></head><body><p class="color: red"><p/></body></html>"""

response = "HTTP/1.1 {status} {status_msg}\nDate: {date}\n{content}".format(
status=status, status_msg=status_msg,
date=datetime.datetime.now(),
content=content)

return response.encode('utf-8')


if __name__ == '__main__':
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8086))
s.listen()
conn, addr = s.accept()

buffer_size = 1024
with conn:
print('Connect by', addr)
d_list = []
while True:
data = conn.recv(buffer_size)
if len(data) < buffer_size:
break

http_data = parse(data)
print(http_data)
response = handle(http_data['url'])

conn.sendall(response)
s.close()

测试结果:

1
2
3
D:\env\Python38\python.exe D:/service/material/socket/http_server.py
Connect by ('127.0.0.1', 64718)
{'method': 'GET', 'url': '/', 'http_version': 'HTTP/1.1', 'header': 'Host: 127.0.0.1:8086'}

到此为止,只是实现了简单的http服务,http协议还有很多的约束用于数据传输。但是本质上所有的web服务器底层的工作原理都是这样。
为了提高服务器性能,流行的web框架采用了各种各样的技术,但这些技术本质上也是操作系统提供的api而已。

下一次,我们将继续丰富我们的web服务器,添加更多的http支持,完成HTTPServer与HTTPClient类。

下载代码:

参考资料: