TCP HTTP 详细内存分析 & time_wait setsockopt & RPC机制

 http://www.kegel.com/c10k.html#nb.edge

 http://www.chinasb.org/archives/2012/11/4954.shtml

 

in_addr_t 一般为32位的unsigned int.

#define IPV4(a,b,c,d) ((a<<0)|(b<<8)|(c<<16)|(d<<24))

 unsigned int value=IPV4(127,0,0,1);    //这里是已逗号分开。

in_addr_t ip;

memcpy(&ip,&value,sizeof(value));

//最终显示127.0.0.1

printf("the ip value is %s",inet_ntoa(*((struct in_addr*)&ip)));

 

UDP协议:发送进程在发送每个数据报的时候立即发送出去,并不等待多个数据报堆积在一起以一个较大数据报发送出去,它是记录型的协议。

TCP协议:发送进程在发送每个数据报的时候在内核处理过程中有可能并不立即发送出去,而是会将多个数据报集中在一起以一个较大的数据报来发送,它是字节流的协议。

发送接收方式
1.异步
报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:

(1)异步双工:接收和发送在同一个程序中,有两个不同的子进程分别负责发送和接收
(2)异步单工:接收和发送是用两个不同的程序来完成。

2.同步
报文发送和接收是同步进行,既报文发送后等待接收返回报文。同步方式一般需要考虑超时问题,即报文发上去后不能无限等
待,需要设定超时时间,超过该时间发送方不再等待读返回报文,直接通知超时返回。

 

http下,基本上就是请求处理,每个http请求都是同步处理的,处理完成一次性返回结果,无法主动推送内容给客户端,只能是客户端不停的轮询。所以并发难上去,实时性达不到,性能也有点逊色,但是编码简单,上手容易。现在的公司早先一款页游,也是基于http的,但是游戏需要大量实时的消息推送,因此又架了个实时服务器,客户端socket长连接实时服务器,逻辑服务器把内容推送到实时服务器再由实时服务器广播给flash客户端,这样就比较麻烦费劲。做交互性,实时性的游戏,socket还是最合适的。

      在socket长连接下,客户端和服务端建立一个长连接,服务端可以随时把消息内容推送给客户端,客户端也可以随时发送给服务端,不需要不停的建立断开连接,节省了部分资源,最重要的,实时性得到很好的实现。缺点是,一台服务器的总连接数是固定的,到达一定数目后就会有瓶颈.

 什么时候用短连接呢?
一般长连接用于少数client  to server   的频繁的通信,例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源。

RFC 1945定义了HTTP/1.0版本。其中最著名的就是RFC 2616RFC 2616定义了今天普遍使用的一个版本——HTTP 1.1HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议。HTTP协议永远都是客户端发起请求,服务器回送响应。

1. HTTP的几个重要概念

1.1 连接:Connection

一个传输层的实际环流,它是建立在两个相互通讯的应用程序之间。

http1.1requestreponse头中都有可能出现一个connection的头,此header的含义是当clientserver通信时对于长链接如何进行处理。

http1.1中,clientserver都是默认对方支持长链接的, 如果client使用http1.1协议,但又不希望使用长链接,则需要在header中指明connection的值为close;如果server方也不想支持长链接,则在response中也需要明确说明connection的值为close。不论request还是responseheader中包含了值为closeconnection,都表明当前正在使用的tcp链接在当天请求处理完毕后会被断掉。以后client再进行新的请求时就必须创建新的tcp链接了。

1.2 消息:Message

HTTP通讯的基本单位,包括一个结构化的八元组序列并通过连接传输。

1.3 请求方法

       HTTP的请求方法包括如下几种:

q      GET   q      POST  q      HEAD  q      PUT  q      DELETE    q      OPTIONS   q      TRACE     q      CONNECT 

2.1 使用telnet进行http测试

       Windows下,可使用命令窗口进行http简单测试。

       输入cmd进入命令窗口,在命令行键入如下命令后按回车:

telnet www.baidu.com 80

       而后在窗口中按下Ctrl+]后按回车可让返回结果回显。

接着开始发请求消息,例如发送如下请求消息请求baidu的首页消息,使用的HTTP协议为HTTP/1.1

GET /index.html HTTP/1.1

 

2.5 常用的请求方式

       常用的请求方式是GETPOST.

l         GET方式:是以实体的方式得到由请求URI所指定资源的信息,如果请求URI只是一个数据产生过程,那么最终要在响应实体中返回的是处理过程的结果所指向的资源,而不是处理过程的描述。

l         POST方式:用来向目的服务器发出请求,要求它接受被附在请求后的实体,并把它当作请求队列中请求URI所指定资源的附加新子项,Post被设计成用统一的方法实现下列功能:

1:对现有资源的解释;

2:向电子公告栏、新闻组、邮件列表或类似讨论组发信息;

3:提交数据块;

4:通过附加操作来扩展数据库 

从上面描述可以看出,Get是向服务器发索取数据的一种请求;而Post是向服务器提交数据的一种请求,要提交的数据位于信息头后面的实体中。

GETPOST方法有以下区别:

1   在客户端,Get方式在通过URL提交数据,数据在URL中可以看到;POST方式,数据放置在HTML HEADER内提交

2   GET方式提交的数据最多只能有1024字节,而POST则没有此限制。

3   安全性问题。正如在(1)中提到,使用 Get 的时候,参数会显示在地址栏上,而 Post 不会。所以,如果这些数据是中文数据而且是非敏感数据,那么使用get;如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用 post为好。

4   安全的和幂等的。所谓安全的意味着该操作用于获取信息而非修改信息。幂等的意味着对同一 URL 的多个请求应该返回同样的结果。完整的定义并不像看起来那样严格。换句话说,GET 请求一般不应产生副作用。从根本上讲,其目标是当用户打开一个链接时,她可以确信从自身的角度来看没有改变资源。比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。反之亦然。POST 请求就不那么轻松了。POST 表示可能改变服务器上的资源的请求。仍然以新闻站点为例,读者对文章的注解应该通过 POST 请求实现,因为在注解提交之后站点已经不同了(比方说文章下面出现一条注解)。

3. 深入了解篇

3.1 CookieSession

CookieSession都为了用来保存状态信息,都是保存客户端状态的机制,它们都是为了解决HTTP无状态的问题而所做的努力。Session可以用Cookie来实现,也可以用URL回写的机制来实现。用Cookie来实现的Session可以认为是对Cookie更高级的应用。

CookieSession有以下明显的不同点:

1Cookie将状态保存在客户端,Session将状态保存在服务器端;

2Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。Cookie最早在RFC2109中实现,后续RFC2965做了增强。网络服务器用HTTP头向客户端发送cookies,在客户终端,浏览器解析这些cookies并将它们保存为一个本地文件,它会自动将同一服务器的任何请求缚上这些cookiesSession并没有在HTTP的协议中定义;

3Session是针对每一个用户的,变量的值保存在服务器上,用一个sessionID来区分是哪个用户session变量,这个值是通过用户的浏览器在访问的时候返回给服务器,当客户禁用cookie时,这个值也可能设置为由get来返回给服务器;

4)就安全性来说:当你访问一个使用session 的站点,同时在自己机子上建立一个cookie,建议在服务器端的SESSION机制更安全些.因为它不会任意读取客户存储的信息。

 

 

 TCP sockect  内存使用情况

  • 如何标识一个TCP连接:系统用一个4四元组来唯一标识一个TCP连接:{local ip, local port,remote ip,remote port}。
  • server最大tcp连接数:server通常固定在某个本地端口上监听,等待client的连接请求。不考虑地址重用(unix的SO_REUSEADDR选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,因此server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方。

服务端,连接达到一定数量,诸如50W时,有些隐藏很深的问题,就不断的抛出来。 通过查看dmesg命令查看,发现大量TCP: too many of orphaned sockets错误,也很正常,下面到了需要调整tcp socket参数的时候了。

第一个需要调整的是tcp_rmem,即TCP读取缓冲区,单位为字节,查看默认值

cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 4161536

默认值为87380bit ≈ 86K,最小为4096bit=4K,最大值为4064K。

第二个需要调整的是tcp_wmem,发送缓冲区,单位是字节,默认值

cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4161536

解释同上

第三个需要调整的tcp_mem,调整TCP的内存大小,其单位是页,1页等于4096字节。系统默认值:

cat /proc/sys/net/ipv4/tcp_mem
932448 1243264 1864896

tcp_mem(3个INTEGER变量):low, pressure, high

  • low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。
  • pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入pressure模式,当内存消耗低于low值时则退出pressure状态。
  • high:允许所有tcp sockets用于排队缓冲数据报的页面量,当内存占用超过此值,系统拒绝分配socket,后台日志输出“TCP: too many of orphaned sockets”。

一般情况下这些值是在系统启动时根据系统内存数量计算得到的。 根据当前tcp_mem最大内存页面数是1864896,当内存为(1864896*4)/1024K=7284.75M时,系统将无法为新的socket连接分配内存,即TCP连接将被拒绝。

实际测试环境中,据观察大概在99万个连接左右的时候(零头不算),进程被杀死,触发out of socket memory错误(dmesg命令查看获得)。每一个连接大致占用7.5K内存(下面给出计算方式),大致可算的此时内存占用情况(990000 * 7.5 / 1024K = 7251M)。

这样和tcp_mem最大页面值数量比较吻合,因此此值也需要修改。

三个TCP调整语句为:

echo "net.ipv4.tcp_mem = 786432 2097152 3145728">> /etc/sysctl.conf
echo "net.ipv4.tcp_rmem = 4096 4096 16777216">> /etc/sysctl.conf
echo "net.ipv4.tcp_wmem = 4096 4096 16777216">> /etc/sysctl.conf

备注: 为了节省内存,设置tcp读、写缓冲区都为4K大小,tcp_mem三个值分别为3G 8G 16G,tcp_rmemtcp_wmem最大值也是16G。

目标达成

 free -m 

top -p 某刻

获取当前socket连接状态统计信息:

cat /proc/net/sockstat

获取当前系统打开的文件句柄:

sysctl -a | grep file
 


TIME_WAIT:

tcp建立连接需要3个握手,但是关闭连接需要4个握手,关闭连接后主动关闭的一方会处于time_wait状态一段时间,这个是可以设置的,好像windows最少是30s,time_wait状态的目的以下两:

1。防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)

2。可靠的关闭TCP连接
在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 RST 而不是 ACK。

所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED。

 

setsockopt()

int PASCAL FAR setsockopt(SOCKET s, int level, int optname, const char FAR *optval, int optlen);
s:标识一个套接字的描述符。
level:选项定义的层次;目前仅支持SOL_SOCKET和IPPROTO_TCP层次。
optname:需设置的选项。
optval:指针,指向存放选项值的缓冲区
optlen:optval缓冲区长度。
⒈设置调用closesocket()后,仍可继续重用该socket。调用closesocket()一般不会立即关闭socket,而经历TIME_WAIT的过程。
  BOOL bReuseaddr = TRUE;
  setsockopt(s,SOL_SOCKET,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));
⒉ 如果要已经处于连接状态的soket在调用closesocket()后强制关闭,不经历TIME_WAIT的过程:
BOOL bDontLinger = FALSE;
setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));
⒊在send(),recv()过程中有时由于网络状况等原因,收发不能预期进行,可以设置收发时限:
int nNetTimeout = 1000; //1秒
//发送时限
setsockopt(socket,SOL_SOCKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
//接收时限
setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
⒋在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节(异步);系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中如果发送或是接收的数据量比较大,可以设置socket缓冲区,避免send(),recv()不断的循环收发:
// 接收缓冲区
int nRecvBuf = 32 * 1024; //设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf = 32*1024; //设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
⒌在发送数据的时,不执行由系统缓冲区socket缓冲区的拷贝,以提高程序的性能:
int nZero = 0;
setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));
⒍在接收数据时,不执行将socket缓冲区的内容拷贝到系统缓冲区:
int nZero = 0;
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
⒎一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:
BOOL bBroadcast = TRUE;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));
⒏在client连接服务器过程中,如果处于非阻塞模式下的socketconnect()的过程中可以设置connect()延时,直到accpet()被调用(此设置只有在非阻塞的过程中有显著的作用,在阻塞的函数调用中作用不大)
BOOL bConditionalAccept = TRUE;
setsockopt(s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));
⒐如果在发送数据的过程中send()没有完成,还有数据没发送,而调用了closesocket(),以前一般采取的措施是shutdown(s,SD_BOTH),但是数据将会丢失。
某些具体程序要求待未发送完的数据发送出去后再关闭socket,可通过设置让程序满足要求:
struct linger {
  u_short l_onoff;
  u_short l_linger;
};
linger m_sLinger;
m_sLinger.l_onoff = 1;
//在调用closesocket()时还有数据未发送完,允许等待.
//若m_sLinger.l_onoff=0;则调用closesocket()后强制关闭
m_sLinger.l_linger = 5; //设置等待时间为5秒
setsockopt(s, SOL_SOCKET, SO_LINGER, (const char*)&m_sLinger, sizeof(linger));

 


 

RPC 允许进程间通信,机制屏蔽了底层,

简单来说,当机器 A 上的进程调用机器 B 上的进程时,A 上的调用进程被挂起,而 B 上的被调用进程开始执行。通过参数将信息传送给被调用方,然后可以通过被调用方传回的结果得到返回。RPC 框架屏蔽了底层传输方式(TCP/UDP)、序列化和反序列化(XML/JSON/二进制)等内容,使用框架只需要知道被调用者的地址和接口就可以了,无须额外地为这些底层内部编程。 
目前主流的 RPC 框架有如下几种。

    • Thrift:Facebook 开源的跨语言框架
    • gRPC:Google 基于 HTTP/2 和 Protobuf 的能用框架
    • Avro:Hadoop 的子项目

 

所有这些步骤的效果是,将客户过程对客户桩发出的本地调用转换成对服务器过程的本地调用,而客户端和服务器都不会意识到有中间步骤的存在。 
这个时候,你可能会想,既然是调用另一台机器的服务,使用 RESTful API 也可以实现啊,为什么要选择 RPC 呢?我们可以从两个方面对比:

    • 资源粒度。RPC 就像本地方法调用,RESTful API 每一次添加接口都可能需要额外地组织开放接口的数据,这相当于在应用视图中再写了一次方法调用,而且它还需要维护开发接口的资源粒度、权限等。
    • 流量消耗。RESTful API 在应用层使用 HTTP 协议,哪怕使用轻型、高效、传输效率高的 JSON 也会消耗较大的流量,而 RPC 传输既可以使用 TCP 也可以使用 UDP,而且协议一般使用二制度编码,大大降低了数据的大小,减少流量消耗。

 

posted on 2016-01-22 11:05  eksay  阅读(986)  评论(0编辑  收藏  举报

导航