网络编程实战
盛延敏
前大众点评云平台首席架构师
立即订阅
6034 人已学习
课程目录
已完结 39 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 学好网络编程,需要掌握哪些核心问题?
免费
第一模块:基础篇 (9讲)
01 | 追古溯源:TCP/IP和Linux是如何改变世界的?
02 | 网络编程模型:认识客户端-服务器网络模型的基本概念
03丨套接字和地址:像电话和电话号码一样理解它们
04 | TCP三次握手:怎么使用套接字格式建立连接?
05 | 使用套接字进行读写:开始交流吧
06 | 嗨,别忘了UDP这个小兄弟
07 | What? 还有本地套接字?
08 | 工欲善其事必先利其器:学会使用各种工具
09丨答疑篇:学习网络编程前,需要准备哪些东西?
第二模块:提高篇 (10讲)
10 | TIME_WAIT:隐藏在细节下的魔鬼
11 | 优雅地关闭还是粗暴地关闭 ?
12 | 连接无效:使用Keep-Alive还是应用心跳来检测?
13 | 小数据包应对之策:理解TCP协议中的动态数据传输
14丨UDP也可以是“已连接”?
15 | 怎么老是出现“地址已经被使用”?
16 | 如何理解TCP的“流”?
17 | TCP并不总是“可靠”的?
18 | 防人之心不可无:检查数据的有效性
19丨提高篇答疑:如何理解TCP四次挥手?
期中复习周 (2讲)
期中大作业丨动手编写一个自己的程序吧!
免费
期中大作业丨题目以及解答剖析
免费
第三模块:性能篇 (12讲)
20 | 大名⿍⿍的select:看我如何同时感知多个I/O事件
21 | poll:另一种I/O多路复用
22 | 非阻塞I/O:提升性能的加速器
23 | Linux利器:epoll的前世今生
24 | C10K问题:高并发模型设计
25 | 使用阻塞I/O和进程模型:最传统的方式
26 | 使用阻塞I/O和线程模型:换一种轻量的方式
27 | I/O多路复用遇上线程:使用poll单线程处理所有I/O事件
28 | I/O多路复用进阶:子线程使用poll处理连接I/O事件
29 | 渐入佳境:使用epoll和多线程模型
30 | 真正的大杀器:异步I/O探索
31丨性能篇答疑:epoll源码深度剖析
第四模块:实战篇 (4讲)
32 | 自己动手写高性能HTTP服务器(一):设计和思路
33 | 自己动手写高性能HTTP服务器(二):I/O模型和多线程模型实现
34 | 自己动手写高性能HTTP服务器(三):TCP字节流处理和HTTP协议实现
35 | 答疑:编写高性能网络编程框架时,都需要注意哪些问题?
结束语 (1讲)
结束语丨我相信这不是结束,让我们江湖再见
网络编程实战
登录|注册

17 | TCP并不总是“可靠”的?

盛延敏 2019-09-09
你好,我是盛延敏,这里是网络编程实战第 17 讲,欢迎回来。
在前面一讲中,我们讲到如何理解 TCP 数据流的本质,进而引出了报文格式和解析。在这一讲里,我们讨论通过如何增强读写操作,以处理各种“不可靠”的场景。

TCP 是可靠的?

你可能会认为,TCP 是一种可靠的协议,这种可靠体现在端到端的通信上。这似乎给我们带来了一种错觉,从发送端来看,应用程序通过调用 send 函数发送的数据流总能可靠地到达接收端;而从接收端来看,总是可以把对端发送的数据流完整无损地传递给应用程序来处理。
事实上,如果我们对 TCP 传输环节进行详细的分析,你就会沮丧地发现,上述论断是不正确的。
前面我们已经了解,发送端通过调用 send 函数之后,数据流并没有马上通过网络传输出去,而是存储在套接字的发送缓冲区中,由网络协议栈决定何时发送、如何发送。当对应的数据发送给接收端,接收端回应 ACK,存储在发送缓冲区的这部分数据就可以删除了,但是,发送端并无法获取对应数据流的 ACK 情况,也就是说,发送端没有办法判断对端的接收方是否已经接收发送的数据流,如果需要知道这部分信息,就必须在应用层自己添加处理逻辑,例如显式的报文确认机制。
从接收端来说,也没有办法保证 ACK 过的数据部分可以被应用程序处理,因为数据需要接收端程序从接收缓冲区中拷贝,可能出现的状况是,已经 ACK 的数据保存在接收端缓冲区中,接收端处理程序突然崩溃了,这部分数据就没有办法被应用程序继续处理。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《网络编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(17)

  • 传说中的成大大
    看到后面我好像理解了我上面那个提问,当崩溃重启过后是重新三次握手建立连接,创建新的套接字,只是在网络上传输的包,因为是通过ip地址和端口方式进行的寻址,所以新连接上去的客户端会接收到之前还没接收到的包,然后新连接的客户端没有这些包的tcp分组信息所以就会给服务器端(对端)发送一个RST

    作者回复: 正解。

    2019-09-09
    6
  • tim
    >>"但是,发送端并无法获取对应数据流的 ACK 情况"
    对上面这段话不理解,TCP 的 ACK不是带着序号的吗?发送端根据这个序号能计算出是哪次发送的ACK。
    哪位大牛能解释一下吗?

    作者回复: 这里是从应用层报文数据角度出发来说的,因为数据是一个流,没有办法判断数据的哪些部分没对端收到。

    从TCP角度来说,确实是通过ACK来感知TCP包的接收情况的。

    2019-10-09
    2
    2
  • 张立华
    这篇文章提醒我们的是,从代码的角度将注意三点:
    1,程序启动的时候,忽略 SIGPIPE
    2,read, write 要判断返回值,根据返回值知道socket断开了
    3,应用层使用 心跳包,心跳包达到超时阀值,则认定socket断开了
    2019-09-20
    2
  • yusuf
    # uname -a
    Linux tst 3.10.0-957.21.3.el7.x86_64 #1 SMP Tue Jun 18 16:35:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
    #
    # ./reliable_client01 127.0.0.1
    good
    peer connection closed
    #
    # ./reliable_client01 127.0.0.1
    bad
    bad
    bad2
    peer connection closed
    #
    # ./reliable_client02 127.0.0.1
    send into buffer 19
    send into buffer -1
    send error: Connection reset by peer (104)

    作者回复: 基本和我的Linux下结果一致。

    2019-09-09
    1
  • godtrue
    假如让你设计一个网络通信协议,你会怎么设计?

    作者回复: 这个问题很大。。。。

    2019-11-23
  • 林林
    老师好,有个问题想请教下。
    有进程A和B(在同一物理机下),A会监听B的连接。在我对进程A kill -2后,B与A的连接一直保持着ESTABILISH, 进程A重启时,绑定监听端口会出现端口被占用的异常。

    这种情况是否表示B没有收到A的fin包,所以才一直保持着ESTABILISH的状态? 这种情况应该怎么解决?

    如果我给进程B加上一个对A的心跳检测,能否解决这个问题?

    期待老师能给我答疑

    作者回复: kill掉A进程后,B进程如果收不到FIN包,它的却能保持ESTABLISHED状态,通过对链路进行读写可以感知链路的状态,当然,你也可以加上心跳检测,检测出链路的状态。

    2019-11-20
  • JasonZhi
    老师你好,文章的最后一个例子还有一些疑问。我的理解是对于一个已经关闭的socket ,执行两次write ,第一次write 会导致对端返回RST,第二次write 由于对收到RST的socket 执行写操作,会触发SIGPIPE。但是为什么例子却说会返回peer reset的错误呢?是不是我哪里理解错了。

    作者回复: 如文中所说,我在MAC系统上实验的结果却是是SIGPIPE,但是在Linux 4.4内核上的实验结果却是直接返回了RST结果,传统教科书都是说应该返回SIGPIPE,但是我得到的Linux 4.4上的结果却不是这样的。

    我建议你通过实验再尝试一下。

    2019-10-16
  • Leon📷
    老师,你文章的案例默认fd都是阻塞的吧,如果是非阻塞的话,返回的n < 0 不一定是错误啊

    作者回复: 到现在为止,都是阻塞的,后面会切成非阻塞的。

    2019-10-13
  • JasonZhi
    老师你好,我在linux环境下测试最后一个例子时,在客户端代码的count--后面加上sleep(4),那么关闭服务端后,连续向服务端写入就会返回PIPIE错误,并且会收到SIGPIPIE信号,能解释其中原因吗?
    贴上代码:
     while (count > 0) {
            n_written = send(socket_fd, msg, strlen(msg), 0);
            fprintf(stdout, "send into buffer %ld \n", n_written);

            if (n_written <= 0) {
                error(1, errno, "send error");
                return -1;
            }
            count--;

            sleep(4);
        }

    作者回复: 休眠4秒之后往一个已经关闭的套接字记下写数据,于是得到了SIGPIPE信号。

    2019-10-09
    1
  • W.T
    老师,最后一行的Connection reset by peer是从哪里打印输出的?是内核协议栈打印输出到终端上的吗?在程序代码中没找到对应的printf语句

    作者回复: 是打印出来的error信息,
    if (err)
       fprintf(stderr, ": %s (%d)\n", strerror(err), err);

    2019-09-22
  • 星辰
    老师 我提一个read直接感知FIN包的疑问哈:

    我停留在 stdin这里 等我输入完之后,就能调用read感知到对端已经关闭了呀? 是因为等到stdin之后,再感知是不是太晚了呀?

    作者回复: 这就是为什么需要使用select、poll等事件分发机制,正确的解法是一旦有事件发生,比如这里read读到EOF,就应用直接去处理此类事件,而不是阻塞在这里等待用户的输入。好消息是,很快我们就会详细学习这部分内容了。

    2019-09-16
  • 徐凯
    第二题 客户端--------服务器

    1.  客户端发送FIN包,处于发送缓冲区的数据会逐一发送(可能通过一次或多次write操作发送),FIN包处于这段数据的末尾,当数据到达接收端的接收缓冲区时,FIN起到了一个结束符的作用,当接收端接收数据时遇到FIN包,read操作返回EOF通知应用层。然后接收端返回一个ACK表示对这次发送的确认。(此时客户端进入FIN_WAIT1,服务端进入CLOSE_WAIT状态)

    2.  客户端接收到ACK之后,关闭自己的发送通道,客户端此时处于半关闭状态。等待服务器发送FIN包。

      (客户端进入FIN_WAIT2状态)

    3.  服务端发送FIN包,同上类似处于发送缓冲区的内容会连同FIN包一起发过去,当客户端接收成功后同时将FIN解析为EOF信号使得上层调用返回。(客户端进入TIME_WAIT状态 服务端进入LAST_ACK状态)

    4. 客户端等待2MSL的时间,在此期间向服务器发送ACK。如果丢包进行重传。如果服务器收到ACK后 服务器进入CLOSED状态 客户端也进入CLOSED状态。

    5. 连接关闭
    我想问一下 如果最后一次挥手一直丢包 在2MSL的时间内都没到 TCP会咋办 会重置计时器么 还是就不管了直接关闭呢

    作者回复: 我猜想是会直接关闭的,没有对ACK的ACK包。

    2019-09-11
  • 传说中的成大大
    Linux VM-0-13-ubuntu 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
    这个是客户端不停的发送然后服务器端突然关闭
    ./client: send error: Connection reset by peer
    2019-09-09
  • 传说中的成大大
    第一问因为要写代码所以等会来回答先回答第二问
    其实服务器正常退出和异常突出我觉得都差不多,都需要靠read或者write去感知,如果对方已经断开连接就会发送一个fin到本方的接收缓冲区变为eof,read函数也返回0,调用write会失败
    2019-09-09
  • 传说中的成大大
    其实我也一直想问 比如 我客户端突然崩溃了 然后再启动客户端连接上服务器 到底是新建立一个链接 还是老的连接 并且发送rst?

    作者回复: 你已经回答自己的问题了,当然是新的连接。

    2019-09-09
  • 石将从
    这篇读了几遍还是很懵,很多概念理不清楚,不知道对端到底是服务器端还是客户端,都混淆了

    作者回复: 因为TCP是双向的,对端是一个相对的概念,连接建立之后,服务器和客户端彼此互为对方的对端。

    建议你再多理几遍,欢迎提具体的问题。

    2019-09-09
    1
  • 刘晓林
    没有看出“通过 write 产生 RST,read 调用感知 RST”和“向一个已关闭连接连续写,产生SIGPIPE”二者有什么区别呀。前者两个write之间多了一次read,为何在linux下的返回结果就不一样了呀?

    作者回复: 这里想要表达的就是通过read/write来感知TCP链路的状态,一个是通过read来感知,一个是在没有read的情况下使用write来感知。

    2019-09-09
    1
收起评论
17
返回
顶部