网络通信的本质是两台计算机上的两个进程之间的通信。比如,浏览器进程和新浪服务器上的某个Web服务进程在通信,而QQ进程是和腾讯的某个服务器上的某个进程在通信。

当我们访问新浪的时候,发生了什么?

本地电脑上的一个进程(浏览器)向 新浪的服务器发起一个tcp的连接请求。这个请求的格式是什么?

下面写一个python实现的例子,建立一个socket,然后连接新浪,连接之后,发送一个字符串。代码如下:

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('www.sina.com.cn',80)) s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n') 

当建立连接之后,本地进程向新浪的服务器发送的消息的格式是上面这段代码。

GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n 

这个字符串,其实就是http协议的 request请求。


下面讨论的是http协议的格式:

http协议分成两个大的部分,一个是请求,一个是相应。无论是请求还是相应都包含两个部分,一个是header,另外一个是body。(body是可选 的)

【HTTP】超简洁的实例 ——关于HTTP协议分析_#include

HTTP GET请求的格式:

GET /path HTTP/1.1 Header1: Value1 Header2: Value2 Header3: Value3 

注意:每个Header一行一个,换行符是\r\n。

HTTP POST请求的格式:

POST /path HTTP/1.1 Header1: Value1 Header2: Value2 Header3: Value3  body data goes here... 

注意:当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body。

HTTP响应的格式:

200 OK Header1: Value1 Header2: Value2 Header3: Value3  body data goes here... 

再次注意:HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。

请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。


通过上面的描述,利用socket写一个小的demo,理解一下http协议

思路:在本地创建一个socket,向新浪的服务器发起连接,然后伪造一个request请求。请求如下:

GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n 

【HTTP】超简洁的实例 ——关于HTTP协议分析_长链接_02

image.png

执行如下代码:

#coding:utf-8 import socket  #創建tcp socket s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #建立鏈接 s.connect(('www.sina.com.cn',80)) s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')  #創建一個buff等待接受 buffer=[] while True:     d=s.recv(1024)     if d:         buffer.append(d)     else:         break; #把接受緩存的數據都保存到data data = b''.join(buffer) print (data)  #斷開socket s.close()  #把網頁的header和body分離 header, html = data.split(b'\r\n\r\n', 1) print(header.decode('utf-8')) # 把接收的数据写入文件: with open('sina.html', 'wb') as f:     f.write(html) 

运行结果:


【HTTP】超简洁的实例 ——关于HTTP协议分析_字段_03

image.png

【HTTP】超简洁的实例 ——关于HTTP协议分析_字段_04

image.png

【HTTP】超简洁的实例 ——关于HTTP协议分析_服务器_05

image.png

PS:

HTTP之状态码

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:

1xx:指示信息--表示请求已接收,继续处理

2xx:成功--表示请求已被成功接收、理解、接受

3xx:重定向--要完成请求必须进行更进一步的操作

4xx:客户端错误--请求有语法错误或请求无法实现

5xx:服务器端错误--服务器未能实现合法的请求

PPS:

常见状态码:

200 OK //客户端请求成功

400 Bad Request //客户端请求有语法错误,不能被服务器所理解

401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用

403 Forbidden //服务器收到请求,但是拒绝提供服务

404 Not Found //请求资源不存在,eg:输入了错误的URL

500 Internal Server Error //服务器发生不可预期的错误

503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

PPPS: 补充一个小例子,提供一个掉坑的例子

打算写一个模拟并发请求的压力测试demo,核心的思路就是多进程+每个进程发送http请求。要做到不错的性能,打算用c去写。

问题是这样的,在构建http请求的时候,

    char buf[1500];     strcpy(request,"GET / HTTP/1.0");     strcat(request,"\r\n");     strcat(request,"User-Agent: WebBench 1.5");     strcat(request,"\r\n");     strcat(request,"Host: localhost");     strcat(request,"\r\n");     //bug 出现在这里,刚开始没有加上这一行。http get请求每一行是通过\r\n 来换行的     //结尾的标识是通过两个\r\r 来表示,但是第一次的时候,我只写了一个。     //但是把请求打印出来,是看不来少了一个\r\n的,一通好找,找不到bug     //最后,我测试用的服务器是nginx,去看nginx的log     //看到一个log里面的状态码是400,400对应的是 请求无效,然后就执行的查请求这个,最够终于找到这个bug     strcat(request,"\r\n");             int rlen=strlen(request); 

我把源码贴在这里,感兴趣的可以复盘一下问题

main.c

#include "socket.c" #include <unistd.h> #include <sys/param.h> #include <rpc/types.h> #include <getopt.h> #include <strings.h> #include <time.h> #include <signal.h> #include <stdio.h> #include <string.h>  #define REQUEST_SIZE 2048 char request[REQUEST_SIZE];   // 发送的构造的HTTP请求   int main(){     char buf[1500];      strcpy(request,"GET / HTTP/1.0");     strcat(request,"\r\n");     strcat(request,"User-Agent: WebBench 1.5");     strcat(request,"\r\n");     strcat(request,"Host: localhost");     strcat(request,"\r\n");     strcat(request,"\r\n");     int rlen=strlen(request);      printf("----test ----- the http request is ----   : \n");     printf("%s",request);     printf("----end  ------\n");       char *host="localhost";     int port=80;     int s=Socket(host,port);     if(s<0){                 printf("error \n");         return -1;     }     else{         printf("ok \n");     }      //write     if(rlen!=write(s,request,rlen)){         printf("fail \n");         close(s);         return -1;     }     printf("write len is %d",rlen);      //read     int i=0;     while(1){         i=read(s,buf,1500);         printf("len i is : %d",i);         if(i<0){             printf("fail \n");             close(s);             return -1;         }         if(i==0){             printf("%s",buf);             printf("read comlete \n");             break;         }         else{             printf("%s",buf);         }     }     close(s);     return 0; } 

socket.c

#include <sys/types.h> #include <sys/socket.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <sys/time.h> #include <string.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <stdarg.h>  int Socket(const char *host, int clientPort) {     int sock;     unsigned long inaddr;     struct sockaddr_in ad;     struct hostent *hp;          memset(&ad, 0, sizeof(ad));     ad.sin_family = AF_INET;          // 将字符串转换为32位二进制网络字节序的IPv4地址     inaddr = inet_addr(host);     if (inaddr != INADDR_NONE)         memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));     else     {         // 使用域名或主机名获取ip地址         hp = gethostbyname(host);         if (hp == NULL)             return -1;         memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);     }     ad.sin_port = htons(clientPort);          sock = socket(AF_INET, SOCK_STREAM, 0);     if (sock < 0)         return sock;     if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0)         return -1;     return sock; } 


作者:zhaozhengcoder


HTTP 头 Connection=close 作用​

有的网站会在服务器运行一段时间后down掉,有很多原因可能造成这种现象:比如tomcat堆和非堆内存设置不足,程序没能释放内存空间造成内存溢出,或者某些进程一直运行没能释放,造成cup资源大量消耗。

但除了程序本身的原因,还有可能是客服端访问造成(当然这个客户端也包含如蜘蛛软件等搜索引擎),如果服务器和客户端建立的是长链接(可以用"netstat -a"命令查看网络访问信息),这就需要对http响应头的connection做一定的设置。

介绍如下:


1. 解释一下:


在http1.1中request和reponse header中都有可能出现一个connection头字段,此header的含义是当client和server通信时对于长链接如何进行处理。

在http1.1中,client和server都是默认对方支持长链接的, 如果client使用http1.1协议,但又不希望使用长链接,则需要在header中指明connection的值为close;如果server方也不想支持长链接,则在response中也需要明确说明connection的值为close.

不论request还是response的header中包含了值为close的connection,都表明当前正在使用的tcp链接在请求处理完毕后会被断掉。以后client再进行新的请求时就必须创建新的tcp链接了。 HTTP Connection的 close设置允许客户端或服务器中任何一方关闭底层的连接双方都会要求在处理请求后关闭它们的TCP连接。


2.如何在程序中设置:


可以在过滤器中加入:response.setHeader("connection", "close");




与之相关:解决服务器产生大量close_wait问题



要解决这个问题的可以修改系统的参数(/etc/sysctl.conf文件),系统默认超时时间的是7200秒,也就是2小时。

默认如下:

tcp_keepalive_time = 7200 seconds (2 hours)

tcp_keepalive_probes = 9

tcp_keepalive_intvl = 75 seconds


意思是如果某个TCP连接在idle 2个小时后,内核才发起probe.如果probe 9次(每次75秒)不成功,内核才彻底放弃,认为该连接已失效


修改后


sysctl -w net.ipv4.tcp_keepalive_time=30

sysctl -w net.ipv4.tcp_keepalive_probes=2

sysctl -w net.ipv4.tcp_keepalive_intvl=2


经过这个修改后,服务器会在短时间里回收没有关闭的tcp连接。


协议设计

消息分包是协议设计的一个工作,协议设计的话题还不少,这里以HTTP协议为例,简要的说说里面设计的点,自己设计的协议也可以对照着有选择的使用,原理是共通的。


由消息头+消息体组成:空行分割HTTP head和body,HTTP头的每一行以\r\n结尾,空行就是\r\n\r\n

消息分包:如上所述,HTTP用Content-Length和Transfer-Encodeing来分包

消息压缩:请求中有Accept-Encoding字段,响应中用Content-Encoding字段表明压缩方式,一般采用gzip压缩

消息加密:https  (SSL: Secure Socket Layer)

消息ID:URL就是消息ID

响应的状态码:第一个数字定义了响应的类别。

    1xx:指示信息--表示请求已接收,继续处理

    2xx:成功--表示请求已被成功接收、理解、接受

    3xx:重定向--要完成请求必须进行更进一步的操作

    4xx:客户端错误--请求有语法错误或请求无法实现

    5xx:服务器端错误--服务器未能实现合法的请求

协议版本号: HTTP/1.1中的1.1就是HTTP 1.1版本

长连接:请求中Connection: keep-alive表示希望服务器保持连接,减少TCP连接的开销

字符集: Content-Type字段表明了字符集,例如: Content-Type: text/html; charset=gb2312

字符转义:URL中的参数需要做URL转义处理,例如http://xx.com/do?name=t%2F%3F%23%3Daa表示name为t/?#=aa