zoukankan      html  css  js  c++  java
  • 编写基于TCP的应用程序

    这似乎是一个非常简单的话题, 就跟"是个人就能做网站"一样, 你可能也认为"是个人就能写使用TCP socket的网络程序". 不过, 下面介绍的几个基本的原理的做法, 你可能并没有理解.

    TCP是一种流式的协议, 简单的说, TCP不检查数据的语义, 更不会检查数据的边界, 而应用层一般使用的是报文协议, 所以会有所谓的"粘包""拆包"问题. 为此, 产生了一些特定的用法和模式.

    任何应用程序, 都必须先进行报文协议设计. 虽然有些人捂上耳朵叫道"我不需要报文协议", 但是, 他还是需要进行报文协议设计. 有几种方式可用来设计报文协议:

    1. 明确声明报文数据的长度.
    2. 使用分隔符.
    3. 发送方发送完数据后关闭连接.

    第3种是socket的特定用法.

    报文设计方法1: 明确声明报文数据的长度

    此种方法一般较为常用, 因为兼容性好性能高. 一会介绍方法2的时候你就知道了. 一般会在数据的最前面用固定的几个字节存储一个二进制整数, 显示后面的数据的长度. 不过, 这是比较接近硬件底层报文协议设计. 应用层一般不这样, 在数据的前端固定几个字节存储ASCII数字, 前端补字符串'0', 或者在数字串后面跟换行符' ', 这是一种和2的方法的混用.

    报文设计方法2: 使用分隔符

    前面介绍方法1的时候提过了, 使用分隔符来分隔报文, 然后在一般的语言都有 split() 函数, 用起来简单. 不过, 使用分隔符有一个缺点, 就是要进行数据转义, 避免报文数据中带有分隔符, 那就不好了. 此种方法还有一个缺点, 就是要遍历每一个字节, 查找分隔符, 性能不好. 介绍方法1的时候, 因为我们明确知道是数字串后面跟换行符, 所以不需要转义, 不会有转义性能损失, 同时数字串一般很短, 也可以忽略遍历性能损失.

    报文设计方法3: 发送方发送完数据后关闭连接

    这是 HTTP 1.0 采用的方式, HTTP 1.0 会在发送完响应后关闭连接(当然, 发送完请求后不能关闭连接, 所以可想而知, HTTP 1.0 必然使用方法1或者方法2, 你可以自己去学习了解). 这种方法不常用, 因为适用场景非常窄, 功能差.

    很难被理解的常用的TCP应用程序惯用法:

    1. 必须使用循环来发送数据

    对于原始的socket, 发送数据的函数是write:

    ssize_t write(int fd, const void *buf, size_t count);

    但write可能只发送你请求的数据的前面一部分, 也就是说, write返回值(表示已发送的的字节数)可能小于参数中的count. 所以, 你应该在循环中调用 write, 并检查返回值. 请认真的看看 APUE(Advanced Programming in the UNIX)的相关内容.

    2. 必须使用循环来接收数据

    读取数据的接口函数:

    ssize_t read(int fd, void *buf, size_t count);

    我常常见到有些人, 因为没有完整地接收到的发送方发送的数据, 而报怨发送方调用了多次write方法. 这是一种错误的报怨, 基于对TCP的错误理解. *无论对方调用多少次write, 你都不能只调用一次read! 即使你把接收缓冲设置为1GB也不行!*

    首先, 发送方调用write, 把数据拷贝到发送方的发送缓冲区, 然后发送方的网络子系统一段(fragment)一段地发送缓冲区中的数据. 接收方的网络子系统将这些数据片段按顺序组装到接收缓冲区中, 一旦进入接收缓冲, 就不存在片段的说法. 接收方调用read方法, 可能读取部分或者全部缓冲区中的数据后返回, 如果只是部分, 这部分的数据和分段没有任何联系 - 记住这一点!

    3. 标准IO接口只调用一次fgets/fputs

    标准IO的gets/puts向上提供了基于报文的接口, 它们检查缓冲区中数据的分隔符' ', 以便分隔出报文. 所以, 当你只调用一次gets就能读取对方调用一次puts发送的数据时, 不要感到惊讶. 标准IO帮你封装了循环读和写.

    4. 总是在字符串的结尾加上''

    如果你想把某一段字节数组当作C字符串来处理, 那么你必须手动地在字符串应该是''的地方加上''. 例如, 如果你认为ptr[0-5](共6个字节的数据, 最后一个字节的值应该是'')是一个字符串, 那么, 在进行处理之前, 应该执行ptr[5] = ''; 注意, 千万不要执行ptr[strlen(ptr)] = ''! 这样, 才能保证无论对方是无意或者恶意地没有包含'', 你都能安全地进行处理. 另外, 不必在接收前执行类似memset(ptr, 0, BUFLEN)的语句, 这样会浪费一丁点的性能, 只修改一个字节总比修改6个或者更多的字节速度更快.

    另见:TCP读取报文不完整的问题分析(粘包,拆包)

    Related posts:

    1. Linux 核心编程 – fsync, write
    2. 使用 jemalloc 编译过程出错的问题
    3. 小心 int 乘法溢出!
    4. iOS 正确接收 HTTP chunked 数据的方法
    5. C++成员函数作为pthread_create参数
    Posted by ideawu at 2009-10-12 15:15:52
  • 相关阅读:
    IIS-Service Unavailable
    复制datatable,把类型变为字符串
    泛类型的使用
    线程间操作无效: 从不是创建控件“button1”的线程访问它。
    .dialog打开时执行方法
    更新系统时间
    复制对象
    如何安装windows服务
    ObjectARX创建文字
    设置cad进度条的arx代码
  • 原文地址:https://www.cnblogs.com/leijiangtao/p/12078561.html
Copyright © 2011-2022 走看看