C# HTTP 厌世请求法

HyperText Transfer Protocol (HTTP) 是一种基于 TCP 协议之上的应用层协议

HTTP 协议将会通过 TCP 传输层协议来传输特定的消息格式,HTTP 服务器将会正确理解 HTTP 客户端所发来的特定格式信息,并且 HTTP 服务器再返回一个特定格式信息给客户端

image.png

这是一个 HTTP 的请求消息格式

  • 请求行: 包括 HTTP 方法, URL, HTTP 版本

  • 请求头行: 包括多个请求头

  • 内容

在 HTTP 1.1 中,Header 必须包括 Host

接下来为了表示厌世精神,我们将会在 C# 中放弃使用 HttpClient, WebClient, WebRequest, HttpWebRequest 等五花八门的方式,转而使用 Socket 类来发送 HTTP 请求

在 C# 中用 Socket 请求可比 C 方便多了 xDDDDD

请确保使用 System.Net.Sockets 这个命名空间,这样我们就能构造一个新的 Socket 了

1
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

AddressFamily.InterNetwork 我们将要使用 IPv4 的方式连接上我们的服务器

SocketType.Stream 数据将会以字节流的方式返回

ProtocolType 为 TCP,因为 HTTP 基于 TCP 协议

根据上面的图,我们需要手动编写 TCP 传输的消息

可以先看一个例子

image.png

请求行中包含方法,我们要请求的 URL 和一个 HTTP 版本

1
GET /get HTTP/1.1

这行结束之后我们需要一个空格符和一个回车符,使用用 \r\n

下一行是请求头,HTTP 1.1 中要求至少包含 Host 请求头

1
Host: httpbin.org

一样的,当请求头结束之后也要加入空格符和回车符

如果你打算结束请求头行了,再次加入空格符和回车符

因此,我们的 TCP 消息结构会写成

1
GET /get HTTP/1.1\r\nHost: httpbin.org\r\n\r\n

写好之后,就可以开始准备发送请求了,为了演示,我将使用 httpbin.org 这个服务

通过

1
socket.Connect("httpbin.org", 80)

可以向 httpbin.org 的 80 端口通信

然后用

1
socket.Send(Encoding.UTF8.GetBytes(httpMessage));

发出 TCP 连接

接着我们可以准备接收服务器回传的消息了

建立一个 response 的变量用于存放 HTTP 返回消息

因为目前还没有回传的消息,可以使用 string 里面的 Empty 字段来声明一个空的字符串类型数据

1
var response = string.Empty;

接下来建立 byte 并且把数据放进 byte 中

1
2
var receivedBytes = new byte[socket.ReceiveBufferSize];
var bytesRange = socket.Receive(receivedBytes);

receivedBytes 的大小应该是 socket 接收到数据的 Buffer 的大小

然后用 socket.Receive 方法将数据放进 receivedBytes,并且拿到有多少 Bytes 在里面

最后通过一个循环把数据转成普通字符串,存进 response 变量中

1
2
3
4
for (int i = 0; i < bytesRange; i++)
{
response += Convert.ToChar(receivedBytes[i]).ToString();
}

最后就可以打印出来了

1
Console.WriteLine(response);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HTTP/1.1 200 OK
Date: Sat, 26 Mar 2022 13:16:15 GMT
Content-Type: application/json
Content-Length: 199
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
"args": {},
"headers": {
"Host": "httpbin.org",
"X-Amzn-Trace-Id": "Root=1-623f121f-3130b9c35588925c532317a6"
},
"origin": "203.198.218.99",
"url": "http://httpbin.org/get"
}

如果需要发送数据时,修改下消息结构就好了

image.png

当我们要发送 JSON 数据的时候,其实只需要加入 Content-Type 头为 application/json 并且最后携带上 JSON 格式的消息即可

回到我们的 HTTP 消息结构,先把 GET 改成 POST,并且别忘了把 URL 的 /get 改成 /post;因为 httpbin.org 接收 POST 请求的路由在 /post 下

加入 Content-Type 的头,内容为 application/json; charset=utf8

并且放入 Content-Length,目前为了演示,我们直接写死 38 就好

最后放入 JSON 数据

1
{"message":"Ja! Ich bin eine Katze."}

新的请求结构是这样的

1
2
var httpMessage =
"POST /post HTTP/1.1\r\nHost: httpbin.org\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 38\r\n\r\n{\"message\": \"Ja! Ich bin eine Katze.\"}";

再次启动就可以拿到新的数据了

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
HTTP/1.1 200 OK
Date: Sat, 26 Mar 2022 13:40:19 GMT
Content-Type: application/json
Content-Length: 432
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
"args": {},
"data": "{\"message\": \"Ja! Ich bin eine Katze.\"}",
"files": {},
"form": {},
"headers": {
"Content-Length": "38",
"Content-Type": "application/json; charset=utf-8",
"Host": "httpbin.org",
"X-Amzn-Trace-Id": "Root=1-623f17c3-2d7c46025329dd3f6cf0ea2f"
},
"json": {
"message": "Ja! Ich bin eine Katze."
},
"origin": "203.198.218.99",
"url": "http://httpbin.org/post"
}

Httpbin 的服务器已经正确的理解了本次请求并且将 JSON 解析放入 json 对象中

最后放上整个程序的 C# 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var httpMessage =
"POST /post HTTP/1.1\r\nHost: httpbin.org\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 38\r\n\r\n{\"message\": \"Ja! Ich bin eine Katze.\"}";

socket.Connect("httpbin.org", 80);
socket.Send(Encoding.UTF8.GetBytes(httpMessage));

var response = string.Empty;
var receivedBytes = new byte[socket.ReceiveBufferSize];
var bytesRange = socket.Receive(receivedBytes);

for (int i = 0; i < bytesRange; i++)
{
response += Convert.ToChar(receivedBytes[i]).ToString();
}

Console.WriteLine(response);