Senlin's Blog


  • 分类

  • 归档

  • 标签

  • 关于

Nagle 算法与 TCP socket 选项 TCP_CORK

发表于 2017-02-10   |   分类于 网络编程   |   阅读次数

Nagle 算法

  Nagle算法由John Nagle在1984年提出,这个算法可以减少网络中小的packet的数量,从而降低网络的拥塞程度。一个常见的例子就是Telnet程序,用户在控制台的每次击键都会发送一个packet,这个packet通常包含41个字节,然而只有一个字节是payload,其余40个字节都是header,如果每次击键都发送一个packet,那就会造成了巨大的开销。
  为了减小这种开销,Nagle算法指出,当TCP发送了一个小的segment(小于MSS),它必须等到接收了对方的ACK之后,才能继续发送另一个小的segment。那么在等待的过程中(一个RTT时间),TCP就能尽量多地将要发送的数据收集在一起,从而减少要发送的segment的数量。
  默认情况下,TCP开启了Nagle算法,然而Nagle算法并不是灵丹妙药,它会增加TCP发送数据的延迟。在一些要求低延迟的应用程序中(例如即时通讯应用),则需要禁用Nagle算法:

1
2
int optval = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));

Delayed ACK

  TCP的 Delayed ACK 与Nagle算法有异曲同工之妙,Delayed ACK很好理解,当TCP接收到数据时,并不会立即发送ACK给对方,相反,它会等待应用层产生数据,以便将ACK和数据一起发送(在Linux最多等待40ms)。
  我们知道,Nagle算法会增加TCP发送数据的延迟,然而,在某些情况下,Delayed ACK会放大这种延迟。一个常见的例子客户端HTTP POST协议,首先看看客户端的代码:

1
2
3
4
write(http_request_header);
write(http_request_body);
// get response from server ...

  客户端调用两次write()来发送HTTP POST请求,服务端则需要调用两次read()读取客户端的HTTP请求:

1
2
3
4
http_request_header = read(...);
http_request_body = read(...);
// write response to client ...

  通常来说,HTTP请求的header和body都是小的segment,但由于TCP默认开启Nagle算法,因此客户端在发送请求的header之后,如果还未收到ACK,则不能发送body。




  Server在收到请求的header之后,由于还没有收到请求的body,因此无法立即产生HTTP响应给客户端,这就导致了http_request_header的ACK大概会延迟40ms才发送。
  为避免这种延迟的出现,需要做两件事:

  • 设置TCP_NODELAY选项。
  • 将客户端的两次write()合并成一个,避免服务端的Delayed ACK。
阅读全文 »

Linux 的 socket 选项SO_REUSEPORT

发表于 2017-02-08   |   分类于 网络编程   |   阅读次数

均衡转发

  多线程服务端编程中经常遇到的一个问题就是,如何将客户端的连接请求均衡地分发给每个线程。一种常见的做法是让多个线程同时调用accept(),在同一个socket描述符上等待连接请求,当请求到达时,内核会随机唤醒一个线程去处理这个请求。(注:Linux 2.6之后,accept()不会导致惊群。) 然而,正如这篇文章所说的,当连接请求到达时,若系统负载过高,内核就可能会以一种很不均衡地方式去唤醒线程。这就导致了有的线程处理太多请求而忙不过来,而有的线程则很空闲。
  Linux 3.9提供了SO_REUSEPORT选项,对于TCP来说,它允许多个listening socket绑定到同一个端口,也就是说,所有线程都可以创建一个listening socket绑定到同一个端口,并且可以调用accept()在这个socket上等待连接请求。使用SO_REUSEPORT的好处就是,由于内核会将客户端的连接请求均衡地分发给每个listening socket,在多线程环境中,这有利于充分利用好每个 CPU 核心。
  SO_REUSEPORT的用法很简单,只要每个listening socket在调用bind()之前设置好这个选项就可以了:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

bind(sockfd, ...);
阅读全文 »

浅谈 Prorobuf 的自动反射功能

发表于 2016-11-12   |   分类于 网络编程   |   阅读次数

Protobuf 的介绍

  Protobuf 是一种序列化工具,用来将程序的数据结构序列化成字节流,方便网络传输。将数据结构序列化成字节流之前,首先你得向 Protobuf 提供一个 .proto文件,用来描述你的数据结构。举个例子,假设你想存储公司员工的信息,那么首先你需要描述一下员工的信息是怎样的:

1
2
3
4
5
6
package company;
message Employee {
required string name = 1;
required string email = 2;
};

  将文件保存为company.proto,接着用 Protobuf 编译器自动生成一个Employee类:

1
protoc --cpp_out . company.proto

  根据生成的Employee类,我们就可以将员工信息序列成字节流,或从字节流读取员工信息。

阅读全文 »

如何可靠关闭 TCP 连接

发表于 2016-09-15   |   分类于 网络编程   |   阅读次数

write 的含义

  用write()向 socket 描述符写入n个字节,write()返回n,这意味着什么呢?你可能这样想,write()返回n,那不就表示对方已经收到n个字节了吗?

1
2
char buffer[1024];
ssize_t n = write(sockfd, buffer, sizeof(buffer));

  然而,实际上write()返回n仅仅保证将n个字节写入内核中sockfd的发送缓冲区。但是内核是否已经将数据转发给网卡,对方是否收到了数据,就无从得知了。

TCP 的可靠关闭

  既然调用write()无法确保对方收到了数据,那么在调用write()之后就调用close()来关闭连接是否合适呢?RFC1122指出,由于 TCP 是全双工的,调用close()时如果有可读数据停留在接收缓冲区,或者在close()之后收到新的数据,数据就无法被读取,这时 TCP 将会发送 RST 通知对方数据已经丢失了。

阅读全文 »

为 connect 设置连接超时

发表于 2016-08-15   |   分类于 网络编程   |   阅读次数

连接超时

  调用connect连接到远程主机时,若网络发生拥塞,TCP可能会执行多次超时重传,这将耗费不短的时间,所以我们会想要为connect设置连接的超时。考虑到select可以设置一个超时时间,所以我们会使用select来实现这个功能。

  我们实现一个函数connect_timeout:

1
2
3
// Return 1 on success, 0 on timeout and -1 on failed
int connect_timeout(int sockfd, const struct sockaddr *addr,
socklen_t addrlen, struct timeval *timeout);

  其实现并不复杂,首先我们将 socket 设置成非阻塞的,然后调用connect,如果connect返回0就说明连接已经建立了,如果返回-1并且错误码是EINPROGRESS,这说明connect正处在建立连接的过程中,返回其它错误码则表示connect调用失败。

  很明显,既然connect正在建立连接,那么它就有可能成功也有可能失败。那么我们可以调用select并且设置一个超时,等待connect建立连接、超时或失败。如果connect成功建立连接,则需要将 socket 重新设置成阻塞的。

阅读全文 »
1…91011…13

高性能

61 日志
13 分类
14 标签
GitHub 知乎
© 2015 - 2022
由 Hexo 强力驱动
主题 - NexT.Mist
  |   总访问量: