网络编程中的read,write函数

关于TCP/IP协议,建议参考Richard Stevens的《TCP/IP Illustrated,vol1》(TCP/IP详解卷1)。

关于第二层面,依然建议Richard Stevens的《Unix network
proggramming,vol1》(Unix网络编程卷1),这两本书公认是Unix网络编程的圣经。

至于第三个层面,UNP的书中有所提及,也有著名的C10K问题,业界也有各种各样的框架和解决方案,本人才疏学浅,在这里就不一一敷述。

本文的重点在于第二个层面,主要总结一下Linux下TCP/IP网络编程中的read/write系统调用的行为,知识来源于自己网络编程的粗浅经验和对《Unix网络编程卷1》相关章节的总结。由于本人接触Linux下网络编程时间不长,错误和疏漏再所难免,望看官不吝赐教。

一. read/write的语义:为什么会阻塞?

先从write说起:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

首先,write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。

write在什么情况下会阻塞?当kernel的该socket的发送缓冲区已满时。对于每个socket,拥有自己的send buffer和receive
buffer。从Linux 2.6开始,两个缓冲区大小都由系统来自动调节(autotuning),但一般在default和max之间浮动。

# 获取socket的发送/接受缓冲区的大小:(后面的值是在我在Linux 2.6.38 x86_64上测试的结果)
sysctl net.core.wmem_default       #126976
sysctl net.core.wmem_max     #131071
sysctl net.core.wmem_default #126976
sysctl net.core.wmem_max #131071

已经发送到网络的数据依然需要暂存在send
buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。接收端将收到的数据暂存在receive
buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive
buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send
buffer填满,write调用阻塞。

一般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调用阻塞。

而read调用的行为相对容易理解,从socket的receive
buffer中拷贝数据到应用程序的buffer中。read调用阻塞,通常是发送端的数据没有到达。

二. blocking(默认)和nonblock模式下read/write行为的区别:

将socket fd设置为nonblock(非阻塞)是在服务器编程中常见的做法,采用blocking
IO并为每一个client创建一个线程的模式开销巨大且可扩展性不佳(带来大量的切换开销),更为通用的做法是采用线程池+Nonblock
I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。

?





1

2

3

4

5

6

7

8

// 设置一个文件描述符为nonblock

int set_nonblocking(int fd)

{

    int flags;

    if ((flags = fcntl(fd, F_GETFL, 0)) == -1)

        flags = 0;

    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);

}

几个重要的结论:

1. read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。

只有当receive
buffer为空时,blocking模式才会等待
,而nonblock模式下会立即返回-1(errno =
EAGAIN或EWOULDBLOCK)

2. blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking
read并不相同)

nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno =
EAGAIN或EWOULDBLOCK)

对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection
reset by peer),这正是下个小节要提到的:

三. read/write对连接异常的反馈行为:

对应用程序来说,与另一进程的TCP通信其实是完全异步的过程:

1. 我并不知道对面什么时候、能否收到我的数据

2. 我不知道什么时候能够收到对面的数据

3. 我不知道什么时候通信结束(主动退出或是异常退出、机器故障、网络故障等等)

对于1和2,采用write() -> read() -> write() -> read()
->...的序列,通过blocking read或者nonblock read+轮询的方式,应用程序基于可以保证正确的处理流程。

对于3,kernel将这些事件的“通知”通过read/write的结果返回给应用层。

 

假设A机器上的一个进程a正在和B机器上的进程b通信:某一时刻a正阻塞在socket的read调用上(或者在nonblock下轮询socket)

当b进程终止时,无论应用程序是否显式关闭了socket(OS会负责在进程结束时关闭所有的文件描述符,对于socket,则会发送一个FIN包到对面)。

”同步通知“:进程a对已经收到FIN的socket调用read,如果已经读完了receive buffer的剩余字节,则会返回EOF:0

”异步通知“:如果进程a正阻塞在read调用上(前面已经提到,此时receive buffer一定为空,因为read在receive
buffer有内容时就会返回),则read调用立即返回EOF,进程a被唤醒。

socket在收到FIN后,虽然调用read会返回EOF,但进程a依然可以其调用write,因为根据TCP协议,收到对方的FIN包只意味着对方不会再发送任何消息
在一个双方正常关闭的流程中,收到FIN包的一端将剩余数据发送给对面(通过一次或多次write),然后关闭socket。

但是事情远远没有想象中简单。优雅地(gracefully)关闭一个TCP连接,不仅仅需要双方的应用程序遵守约定,中间还不能出任何差错。

假如b进程是异常终止的,发送FIN包是OS代劳的,b进程已经不复存在,当机器再次收到该socket的消息时,会回应RST(因为拥有该socket的进程已经终止)。a进程对收到RST的socket调用write时,操作系统会给a进程发送SIGPIPE,默认处理动作是终止进程,知道你的进程为什么毫无征兆地死亡了吧:)

from 《Unix Network programming, vol1》 3rd Edition:

"It is okay to write to a socket that has received a FIN, but it is an
error to write to a socket that has received an RST."

通过以上的叙述,内核通过socket的read/write将双方的连接异常通知到应用层,虽然很不直观,似乎也够用。

这里说一句题外话:

不知道有没有同学会和我有一样的感慨:在写TCP/IP通信时,似乎没怎么考虑连接的终止或错误,只是在read/write错误返回时关闭socket,程序似乎也能正常运行,但某些情况下总是会出奇怪的问题。想完美处理各种错误,却发现怎么也做不对。

原因之一是:socket(或者说TCP/IP栈本身)对错误的反馈能力是有限的。

网络编程中的read,write函数,布布扣,bubuko.com

时间: 2024-10-06 10:31:54

网络编程中的read,write函数的相关文章

网络编程中shut_down和close()函数的区别

在Linux C网络编程中,一共有两种方法来关闭一个已经连接好的网络通信,它们就是close函数和shutdown函数,它们的函数原型分别为: 1 #include<unistd.h> 2 int close(int sockfd) 3 //返回:0--成功, 1--失败 4   5 #include<sys/socket.h> 6 int shutdown(int sockfd, int howto) 7 //返回:0--成功, 1--失败 对一个tcp socket调用clos

一些在PHP网络编程中会遇到的函数简单介绍

PHP网络编程函数介绍 1.     Fgetcsv函数-解析读入的行并找出csv格式的字段. Fgetcsv函数可以解析读入的行并找出csv格式的字段,然后返回一个包含这些字段的数组. Fgetcsv()函数参数的说明: 参数 说明 Handle 必选参数.打开文件后返回文件标识指针 Length 必选参数.指定一行最多显示的字符数,该值大于csv文件最长行的字符数. Delimiter 可选参数.文件的分隔符 Enclosure 可选参数.文件的分隔符 [示例]获取文件中每行用逗号分隔后所获

Linux网络编程中tcp_server和tcp_client函数的封装

本文的主要目的是将server套接字和client套接字的获取,做一个简易的封装,使用C语言完成.   tcp_server   服务器端fd的获取主要分为以下几步: 1.创建socket,这一步仅仅创建一个socket,没有任何特性的属性. 2.绑定网卡和port,一块主机可能有多块网卡,如果我们使用INADDR_ANY,意味着后面接受的TCP连接可以绑定在任意一块网卡上. 例如某台主机的ip地址有两个:192.168.44.136.10.1.1.4,假设绑定的ip采用INADDR_ANY,端

linux网络编程中INADDR_ANY的使用

网络编程中常用到bind函数,需要绑定IP地址,这时可以设置INADDR_ANY INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”.“任意地址”. 也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思. 比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某 个端口,那他要监听哪个网卡地址的端口呢?如果绑定某个具体的ip地址,你只能监听你所设置的ip地

linux网络编程中INADDR_ANY的含义

INADDR_ANY选项 网络编程中常用到bind函数,需要绑定IP地址,这时可以设置INADDR_ANY INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”.“任意地址”. 也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思. 比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某 个端口,那他要监听哪个网卡地址的端口呢?如果绑定某个具体的ip地址,

socket编程之三:socket网络编程中的常用函数

这节本来打算先给出常用函数介绍,再给两个代码实例,写着写着发现越来越长,决定把代码放在下一节. 本节内容持续更新...... 1 socket()函数 原型: int socket(int domain, int type, int protocol); 描述: 类似打开一个文件,返回一个socket描述符,唯一标识一个socket,后面相应的操作都是这用这个socket描述符. 参数: domain:协议族,常用的协议族有AF_INET.AF_INET6.AF_LOCAL.AF_ROUTE等:

Unix网络编程中的五种I/O模型_转

转自:Unix网络编程中的的五种I/O模型 下面主要是把unp第六章介绍的五种I/O模型. 1. 阻塞I/O模型 例如UDP函数recvfrom的内核到应用层.应用层到内核的调用过程是这样的:首先把描述符.接受数据缓冲地址.大小传递给内核,但是如果此时 该与该套接口相应的缓冲区没有数据,这个时候就recvfrom就会卡(阻塞)在这里,知道数据到来的时候,再把数据拷贝到应用层,也就是传进来的地址空 间,如果没有数据到来,就会使该函数阻塞在那里,这就叫做阻塞I/O模型,如下图: 2. 非阻塞I/O模

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

总结下网络编程中关键的细节问题,包含连接建立.连接断开.消息到达.发送消息等等: 连接建立 包括服务端接受 (accept) 新连接和客户端成功发起 (connect) 连接. accept接受连接的问题在本文最后会聊到,这里谈谈connect的关键点:     使用非阻塞连接建立需要注意:     connect/select返回后,可能没有连接上:需要再次确认是否成功连接: 步骤为: 使用异步connect直接连接一次,因为使用了非阻塞,函数立刻返回: 检查返回值,为0成功连接,否则加入到s

网络编程中常见错误码总结

在网络编程中,总有各种需要注意的环节,几乎每个API都要进行异常处理,判断返回值以及错误码来定位是否需要退出. 本文根据自身使用经验,总结以下错误码及其出现场景和一般处理流程. 网络编程的一般性流程如下 : 客户端 : socket -> connect -> write / read 服务端 : socket -> bind -> listen -> accept 还有因为设置属性带来的其它改变,比如非阻塞和套接字选项. EADDRINUSE : 如果你在同一个端口运行了无