热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

网络编程中的关键问题总结

网络编程中的关键问题总结总结下网络编程中关键的细节问题,包含连接建立、连接断开、消息到达、发送消息等等;连接建立包括服务端接受(accept)新连接和客户端成功发起(connect)连接。a

网络编程中的关键问题总结

总结下网络编程中关键的细节问题,包含连接建立、连接断开、消息到达、发送消息等等;

连接建立

包括服务端接受 (accept) 新连接和客户端成功发起 (connect) 连接。
accept接受连接的问题在本文最后会聊到,这里谈谈connect的关键点;
使用非阻塞连接建立需要注意:
connect/select返回后,可能没有连接上;需要再次确认是否成功连接;

步骤为:

  1. 使用异步connect直接连接一次,因为使用了非阻塞,函数立刻返回;
  2. 检查返回值,为0成功连接,否则加入到select/epoll中监控;
  3. 当有写事件时,连接成功;当即可读又可写时,可能是有错误或者连接成功后有数据已经发过来;所以,此时,需要用getsockopt()读取socket的错误选项,二次确认是否真的连接成功:
Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
error = 0;
if ( (n = connect(sockfd, saptr, salen)) <0)
if (errno != EINPROGRESS)
return(-1);

/* Do whatever we want while the connect is taking place. */
if (n == 0)
goto done; /* connect completed immediately */

if ( (n = Select(sockfd+1, &rset, &wset, NULL,
nsec ? &tval : NULL)) == 0) {
close(sockfd); /* timeout */
errno = ETIMEDOUT;
return(-1);
}

if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
//二次确认是否真的连接成功
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) <0)
return(-1); /* Solaris pending error */
} else
err_quit("select error: sockfd not set");

连接断开

包括主动断开 (close 或 shutdown) 和被动断开 (read 返回 0)。

当打算关闭网络连接时,如何能知道对方已经发送了数据自己还没有收到?
在TCP层面解决:主动关闭的时候只使用半关闭shutdown(), 这样,服务端这边之时关闭了写端,还可以正常读;客户端收到关闭的信号后(read返回0),会再调用shutdown关闭整个连接;
在应用层面解决:双方通过某个标记协商,在标记之后不再读写数据,这样就可以完全的关闭连接了;

关闭连接时需要注意的:
是否还有未发送的数据,需要保证应用缓冲区中的数据都发送完毕之后再关闭缓冲区;
TCP缓存区不用我们考虑,因为在调用shutdown或close的时候,TCP的实现是会将TCP的发送缓冲区中的数据都发送出去,然后再发送FIN报文(也可能是组合成一个报文发送);

消息到达

消息到达是最重要的事件;对它的处理决定了网络编程的风格:是阻塞还是非阻塞、分包的处理、应用层的缓冲如何设计等等;

处理分包

所谓分包,就是在一个个字节流消息中如何区分出一个个消息来;
常见的分包方法有:

  1. 固定长度;
  2. 特殊的结尾符,比如字符串的\0,或者回车换行等;
  3. 固定的消息头中指定后续的消息的长度,然后跟上一个消息体内容;
  4. 使用协议本身的格式,比如json格式头尾配对(XML也一样);

字节序转换注意字节对齐

如果传输的是二进制类型,在字节流的缓存区中直接强转可能core dump;因为有的系统访问地址需要字节对齐,不能在任意地址上访问二进制类型(如整形),合理的方式是将其copy到一个本地变量中,然后再做字节序的转换:

int32_t peekInt32() const
{
assert(readableBytes() >= sizeof(int32_t));
int32_t be32 = 0;
::memcpy(&be32,readerIndex_, sizeof(be32) );
return be32toh(be32);
}

应用层缓存区的实现

数据到达时处理需要注意:
socket读事件来到,必须一次将所有的数据都读完,否则会造成一直有可读事件,造成busy-loop;读到的数据当然就需要有个应用层的缓冲区来存放;
因为应用的缓存区是有限的,可以默认设置一个大小,比如2kb,或者根本就不设置初始大小,用多少分配多少;muduo中使用的是vector 来作为缓存区,可以动态增长;

muduo buffer使用的技巧:
buffe采用了vector自动增长的数据结构;
从系统内核中调用的时候,在应用层需要有足够大的缓冲区,最好能一次将系统recv到的缓冲区给读空,一次系统调用就搞定一切事情;
而应用缓冲区考虑到有很多个并发的可能,针对每个连接一次都分配较大的缓冲区浪费严重,陈硕推荐使用readv一次读入到两个地址中,首先将第一个地址填满,如果还有更多数据,就写入到临时缓冲区中,然后append到应用缓冲区;

读的时候使用readv,局部使用一个足够大的额外空间(64KB),这样,一次读取就足以将socket中的缓存区读空(一般不会超过64K,tcp buffer如果确实要设置大的缓存区,需要调整系统参数);如果数据不多,可能内部buffer就装下了,没有额外操作,否则,多的数据读到了外部的缓存区,再append到内部缓存区:

ssize_t Buffer::readFd(int fd, int* savedErrno)
{
// saved an ioctl()/FIONREAD call to tell how much to read
char extrabuf[65536];
struct iovec vec[2];
const size_t writable = writableBytes();
vec[0].iov_base = begin()+writerIndex_;
vec[0].iov_len = writable;
vec[1].iov_base = extrabuf;
vec[1].iov_len = sizeof extrabuf;
// when there is enough space in this buffer, don't read into extrabuf.
// when extrabuf is used, we read 128k-1 bytes at most.
const int iovcnt = (writable //只有一次系统调用:这里的实现比较巧妙
const ssize_t n = sockets::readv(fd, vec, iovcnt);
if (n <0)
{
*savedErrno = errno;
}
else if (implicit_cast(n) <= writable)
{
writerIndex_ += n;
}
else
{
writerIndex_ = buffer_.size();
append(extrabuf, n - writable);
}
// if (n == writable + sizeof extrabuf)
// {
// goto line_30;
// }
return n;
}

发送消息

网络编程中数据发送比数据接受要难处理;
数据的接收,只需要peek足够的数据后,就可以从应用缓冲区接收出来,然后处理;而数据的发送,还需要考虑对方接受缓慢的情况,导致tcp发送缓冲区累积,最终导致应用缓冲区累积;

举个例子:某客户端对echo服务器只发送,但故意不接收;
客户端如果只是发送,但从不接收的话,那么这边发送过去的报文,首先会导致客户端的tcp接收缓冲区满,然后通过ack报文告诉服务器端,这边的滑动窗口为0了,不能再发了;后续客户端发送的报文就把服务器端TCP发送缓冲区积满,然后累积应用层的发送缓冲区(因为是非阻塞),最终导致服务端的应用缓存区满或者内存撑爆;

需要发送数据的时候,优先直接调用write()发送,如果发送不成功,或没有全部发送完毕,才加入到发送缓存区,等待可写事件到来后发送;
直接调用write()发送数据时,需要先将本次需要发送的数据添加到缓存区,然后发送缓存区,不可直接发送本次数据(因为缓存区中可能有遗留的数据未发送完)

void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting())
{
//注意,这里只调用了一次write,而没有反复调用write直到出现EAGAIN错误,
//原因是如果第一次调用没有发送完全部的数据,第二次调用几乎肯定是EAGAIN错误,
//因此这里减少了一次系统调用,这么做不影响正确性,却能够降低系统时延
ssize_t n = sockets::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0)
{
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0)
{
//如果发送缓存区为空,不再关注写事件,避免 busy loop
channel_->disableWriting();
//如果还有写完成之后的回调,加入待执行回调队列
if (writeCompleteCallback_)
{
loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
}
//如果此时正在关闭,调用shutdownInLoop 继续执行关闭过程
if (state_ == kDisconnecting)
{
shutdownInLoop();
}
}
}
else
{
LOG_SYSERR <<"TcpConnection::handleWrite";
// if (state_ == kDisconnecting)
// {
// shutdownInLoop();
// }
}
}
else
{
LOG_TRACE <<"Connection fd = " <fd()
<<" is down, no more writing";
}
}

消息发送完毕

对于低流量的服务,可以不必关心这个事件;另外,这里“发送完毕”是指将数据写入操作系统的缓冲区,后续由 TCP 协议栈负责数据的发送与重传,不代表对方已经收到数据。

其它问题

IO multiplexing 是否可以配合阻塞套接字使用?

一般都配合非阻塞socket使用,如果使用阻塞IO,可能在读写事件上阻塞当前线程,造成无法继续处理已经就绪的事件;
初学网络编程可能都会有这个想法,select返回后,如果是读事件,那么这时候tcp读缓冲区肯定是有数据,这时即使使用阻塞套接字来read,应该也不会阻塞;但这样忽略了一个点,缓冲区确实是有数据,但是很可能到达的数据并不满足你要求读的数据大小,这样read调用还是会阻塞,直到有足够的数据才返回;
那么,对于数据读不可以,对accept()总可以吧,连接事件返回,一般都是有新用户接入,这时候阻塞的accept()应该总是能够返回;但在某些情况下,可能对方刚连接上就断开了,并给服务端发送了一个RST请求,造成服务端这边将已经就绪的连接请求又移除了,这样的场景下,select返回,但是accept却无法获取新的连接,造成阻塞,直到下一个连接请求到来;(这方面的例子详见《UNIX网络编程卷1:套接字联网API》16.6节非阻塞accept() )
所以任何时候,IO multiplexing都需要配合非阻塞IO使用;

零拷贝的实现

对于内核层的实现,底层调用的是系统调用sendFile()方法;
zerocopy技术省去了将操作系统的read buffer拷贝到程序的buffer, 以及从程序buffer拷贝到socket buffer的步骤, 直接将 read buffer 拷贝到 socket buffer;
image
详见:http://www.cnblogs.com/zemliu/p/3695549.html

应用层上的实现,对于自定义的结构,一般是交换内部指针(使用C++11,可以使用move操作来实现高效交换结构体)
如果是vector等结构,使用其成员函数swap()就能达到高效的交换(类似C++11中的move操作);
例如muduo中buffer实现:通过swap实现了缓存区的指针交换,从而达到数据交换的目的,而不用拷贝缓冲区;

void swap(Buffer& rhs)
{
buffer_.swap(rhs.buffer_); // std::vector buffer_;
std::swap(readerIndex_, rhs.readerIndex_);
std::swap(writerIndex_, rhs.writerIndex_);
}

epoll使用LT

epoll使用是LT而非ET,原因如下:

  1. LT编程方便,select的经验都可同样适用;
  2. 读的时候只需要一次系统调用,而ET必须读到EAGAIN错误;减少一次系统调用,降低时延;

一般认为 edge-trigger 模式的优势在于能够减少 epoll 相关系统调用,这话不假,但网络服务程序里可不是只有 epoll 相关系统调用,为了绕过饿死问题,edge-trigger 模式下用户要自行进行 read/write 循环处理,这其中增加的系统调用和减少的 epoll 系统调用加起来,总体性能收益究竟如何?只有实际测量才知道,无法一概而论。为了降低处理逻辑复杂度,常用的事件处理库大部分都选择了 level-trigger 模式(如 libevent、boost::asio、muduo等)

参考

《UNIX网络编程卷1:套接字联网API》
《Linux多线程服务端编程:使用muduo网络库》

Posted by: 大CC | 31DEC,2015
博客:blog.me115.com [订阅]
Github:大CC


推荐阅读
  • 在Delphi7下要制作系统托盘,只能制作一个比较简单的系统托盘,因为ShellAPI文件定义的TNotifyIconData结构体是比较早的版本。定义如下:1234 ... [详细]
  • WinMain 函数详解及示例
    本文详细介绍了 WinMain 函数的参数及其用途,并提供了一个具体的示例代码来解析 WinMain 函数的实现。 ... [详细]
  • 本文介绍如何使用线段树解决洛谷 P1531 我讨厌它问题,重点在于单点更新和区间查询最大值。 ... [详细]
  • 单片微机原理P3:80C51外部拓展系统
      外部拓展其实是个相对来说很好玩的章节,可以真正开始用单片机写程序了,比较重要的是外部存储器拓展,81C55拓展,矩阵键盘,动态显示,DAC和ADC。0.IO接口电路概念与存 ... [详细]
  • MySQL Decimal 类型的最大值解析及其在数据处理中的应用艺术
    在关系型数据库中,表的设计与SQL语句的编写对性能的影响至关重要,甚至可占到90%以上。本文将重点探讨MySQL中Decimal类型的最大值及其在数据处理中的应用技巧,通过实例分析和优化建议,帮助读者深入理解并掌握这一重要知识点。 ... [详细]
  • 开发日志:高效图片压缩与上传技术解析 ... [详细]
  • 在尝试对 QQmlPropertyMap 类进行测试驱动开发时,发现其派生类中无法正常调用槽函数或 Q_INVOKABLE 方法。这可能是由于 QQmlPropertyMap 的内部实现机制导致的,需要进一步研究以找到解决方案。 ... [详细]
  • 深入解析C语言中结构体的内存对齐机制及其优化方法
    为了提高CPU访问效率,C语言中的结构体成员在内存中遵循特定的对齐规则。本文详细解析了这些对齐机制,并探讨了如何通过合理的布局和编译器选项来优化结构体的内存使用,从而提升程序性能。 ... [详细]
  • 在深入探讨进程间通信技术时,本文重点解析了描述符传递的方法。通过详细分析发送和接收描述符的过程,文章首先介绍了发送描述符的具体步骤,并提供了相关函数原型。此外,还讨论了如何高效地在不同进程之间传输文件描述符,以实现资源的共享和同步。这一技术在多进程应用中具有重要意义,能够显著提升系统的性能和可靠性。 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 在使用 Qt 进行 YUV420 图像渲染时,由于 Qt 本身不支持直接绘制 YUV 数据,因此需要借助 QOpenGLWidget 和 OpenGL 技术来实现。通过继承 QOpenGLWidget 类并重写其绘图方法,可以利用 GPU 的高效渲染能力,实现高质量的 YUV420 图像显示。此外,这种方法还能显著提高图像处理的性能和流畅性。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
    字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
author-avatar
keleesen
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有