什么TCP几层模型,什么几次握手就不扯了。让我们从代码层面来解析下http协议,主要就是用socket实现一个客户端的http,通过手动构造socket传输连接数据可以很清晰的明白http的协议内容。
- GET
- 请求协议
简单来说就是所有数据都是一行一行的文本(所以才叫文本协议吧),用\r\n换行,最后结束的时候多一个空行。
假设我们要发送get请:http://127.0.0.1:1234/ad1/ad2/?name=a&age=1
然后其中的headers中包含两个键值对:ABC:123、DEF:456
对应的数据就是这样:
页面路径中?开始为参数,通过&分割参数,每个参数用key=val形式。一般框架都可以自动解析路径中的参数。
-
- 响应协议
和请求协议是一样。需要注意的就是这里的headers内的Content-Length,是比较重要的一个返回参数,用来标记返回消息的数据长度。同理Content-Type也就很重要,一般用来标记返回的数据是什么类型,比如application/json、image/jpeg等等,http是文本传输协议,主要指的是协议头,而内容就可以是二进制流,不需要做成字符串。
假设服务器返回了“funCallback”这个字符串,字符串长度11
对应的主要数据就是这样:
-
- 测试代码
- http服务
- 测试代码
用python写个简单的服务。
from urllib.parse import urlsplit, parse_qs
from http.server import HTTPServer, BaseHTTPRequestHandler
class ProHeader(BaseHTTPRequestHandler):
def do_GET(self):
print("pageaddr: " + self.path)
try:
print("ABC: " + self.headers['ABC']) #获取headers中的ABC参数 123
print("DEF: " + self.headers['DEF']) #获取headers中的ABC参数 456
except Exception as e:
print(e)
rsp = "funCallback"
rsp = rsp.encode("utf8")
self.send_response(200)
self.send_header("Content-Length", str(len(rsp)))
self.end_headers()
self.wfile.write(rsp)
if __name__ == '__main__':
httpd = HTTPServer(('', 1234), ProHeader)
httpd.serve_forever()
-
-
- 客户端请求
-
这里直接用C#的socket,按照协议组合数据,然后进行发送和接收。
Socket sokClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sokClient.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));
StringBuilder sb = new StringBuilder();
sb.Append("GET /ad1/ad2/?name=a&age=1 HTTP/1.1\r\n");//第一行
//n个headers
sb.Append("ABC:123\r\n");
sb.Append("DEF:456\r\n");
sb.Append("\r\n");//末尾空行
//发送请求数据
string datstr = sb.ToString();
byte[] bs = Encoding.UTF8.GetBytes(datstr);
sokClient.Send(bs, bs.Length, 0);
//接收请求返回数据
byte[] by = new byte[1];
MemoryStream msHead = new MemoryStream();
int ContentLength = 0;
while (true)
{
int ret = sokClient.Receive(by);
if (ret <= 0) break;
msHead.Write(by, 0, 1);
string revdata = Encoding.UTF8.GetString(msHead.ToArray());
if (revdata.Contains("\r\n\r\n"))
{//判断为末尾空行,获取返回的数据长度
ContentLength = Convert.ToInt32(Regex.Match(revdata, "(?<=(Content-Length:)).*(?=\r)").Value.ToString());
break;
}
}
//根据ContentLength长度接收数据内容
byte[] revDataBy = new byte[ContentLength];
int revLen = 0;
while (revLen < ContentLength)
{
int read = sokClient.Receive(revDataBy, revLen, ContentLength - revLen, SocketFlags.None);
revLen += read;
}
Console.Write("====head====\n");
Console.WriteLine(Encoding.UTF8.GetString(msHead.ToArray()));
Console.Write("\n====rev data====\n");
Console.WriteLine(Encoding.UTF8.GetString(revDataBy));
-
-
- 运行结果
-
服务器接收数据并解析数据头,结果如下:
客户端发送后,解析服务端的结果如下:
- POST
post和get协议一样,区别就在于post发送时,第一行的请求方法由GET变成了POST。
然后最重要的就是post可以发送大一些的数据,不像get只能在headers和地址中写字符串。
发送时headers内加上Content-Length和Content-Type这种,然后传输完协议头继续就可以传输数据内容了。
我们就简单写一个通过Post发送图像的例子测试一下。
-
- http服务
服务器接收POST请求,然后通过Content-Length直接读发送的数据大小,然后存储成a.jpg就行。
from urllib.parse import urlsplit, parse_qs
from http.server import HTTPServer, BaseHTTPRequestHandler
class ProHeader(BaseHTTPRequestHandler):
def do_POST(self):
jpgdat = self.rfile.read(int(self.headers['Content-Length']))
pf = open("a.jpg","wb")
pf.write(jpgdat)
pf.close()
if __name__ == '__main__':
httpd = HTTPServer(('', 1234), ProHeader)
httpd.serve_forever()
-
- 客户端请求
客户端读取图像,将长度写入headers内的Content-Length,然后发送数据头和内容即可。
FileInfo fi = new FileInfo("D:\\1.jpg");
byte[] jpgbuf = new byte[fi.Length];
fi.OpenRead().Read(jpgbuf, 0, jpgbuf.Length);
StringBuilder sb = new StringBuilder();
sb.Append("POST /ad1 HTTP/1.1\r\n");//第一行
sb.Append("Content-Length:" + jpgbuf.Length.ToString() + "\r\n"); //headers 设置标志数据长度
sb.Append("\r\n");//末尾空行
//发送请求数据
string datstr = sb.ToString();
byte[] bs = Encoding.UTF8.GetBytes(datstr);
Socket sokClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sokClient.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));
//将数据头和数据内容发送
sokClient.Send(bs, bs.Length, 0);
sokClient.Send(jpgbuf, jpgbuf.Length, 0);
Console.ReadLine();