网络编程中的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栈本身)对错误的反馈能力是有限的。

时间: 2024-11-13 06:36:49

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

多线程在Visual C#网络编程中的应用

visual|编程|多线程|网络 网络应用程序的一般都会或多或少的使用到线程,甚至可以说,一个功能稍微强大的网络应用程序总会在其中开出或多或少的线程,如果应用程序中开出的线程数目大于二个,那么就可以把这个程序称之为多线程应用程序.那么为什么在网络应用程序总会和线程交缠在一起呢?这是因为网络应用程序在执行的时候,会遇到很多意想不到的问题,其中最常见的是网络阻塞和网络等待等. 程序在处理这些问题的时候往往需要花费很多的时间,如果不使用线程则程序在执行时的就会表现出如运行速度慢,执行时间长,容易出现错

正则表达式在网络编程中的运用(3)

编程|网络|正则 应用实例 在对正则表达式有了较为全面的了解之后,就可以在Perl,PHP,以及ASP等程式中使用正则表达式了. 下面以PHP语言为例,使用验证用户在线输入的邮件地址以及网址的格式是否正确.PHP 提供了eregi()或ereg()资料处理函数实现字串比对剖析的模式匹配操作ereg()函数的使用格式如下: ereg (pattern, string) 其中,pattern代表正则表达式的模式:而string则是执行查找替换操作的目标对象,如Email地址值.本函式以 patter

fd_read-请教网络编程中FD_READ问题

问题描述 请教网络编程中FD_READ问题 网络编程新手,请各位高人不吝指教 windows编程接收网络发来的消息,在头文件申明了消息映射函数用于处理FD_READ afx_msg LRESULT OnHardTcpSock(WPARAM wParam, LPARAM lParam); 现在的问题是这样的,当上一个FD_READ消息还在OnHardTcpSock函数中处理但尚未处理完成的时候,新的一个FD_READ又到达了,这个时候系统是如何应对的? 1.等待当前FD_READ消息处理完,再去处

处理Linux网络编程中的IP地址

 Linux网络服务能力非常强大,它的TCP/IP代码是最高级的.Linux的网络实现是模仿FreeBSD的,它支持FreeBSD的带有扩展的Sockets(套接字)和TCP/IP协议.它支持两个主机间的网络连接和Sockets通讯模型,实现了两种类型的Sockets:BSD Sockets和INET Sockets.它为不同的通信模型和服务质量提供了两种传输协议,即不可靠的.基于消息的UDP传输协议和可靠的.基于流的传输协议TCP,并且都是在IP网络协议上实现的.INET sockets是在以

正则表达式在网络编程中的运用(1)

编程|网络|正则 [前言:]在我们编写WEB程序时,经常会判断一个字符串的有效性,如:一个串是否是数字.是否是有效的Email地址等等.如果不使用正则表达式,那么判断的程序会很长,并且容易出错,如果使用正则表达式,这些判断就是一件很轻松的工作了.本文全面介绍正则表达式的慨念.格式.并以在PHP.ASP中的应用实例增加读者的感性认识.正则表达式的应用很广,需要大家在学习和实践中不断的总结. 正则表达式简介 简单的说,正则表达式是一种可以用于模式匹配和替换的强有力的工具.在网络编程中应用广泛,如PH

函数在编程中的用处,函数的利弊?

问题描述 函数在编程中的用处,函数的利弊? 请问既然有了数组,为什么我们还要用函数,数组不是就可以表示很多变量了么? 解决方案 函数是函数,数组是数组.函数不是用来表示很多变量,而是用来将代码片段和数据封装成独立重复使用的小构件. 函数相当于一个黑盒,除了输入输出,和主程序上下文无关.这样这些代码不但在这里用,也能在别的地方用. 函数的参数有效地区隔了形参和实参的不同.比如: int add(int n, int m) { return m + n; } 这个函数可以用在add(a,b),或者a

服务器-网络编程中并未建立连接的端口却能往套接字中写,发送数据,困惑

问题描述 网络编程中并未建立连接的端口却能往套接字中写,发送数据,困惑 拜托各位了,碰上一个难以理解的困惑 情况如下 我在qt中编写了一个客户端(抛开qt,也可以理解为别的写的),在这个应用程序中有一个对象是专门用来建立连接并且处理和服务器的相关的通信. 服务器的话是在linux下c写的,简单的可以看做这种模式accept等待连接,连接成功后阻塞读写 while(1) { clientfd = accept(lfd, null, null): printf("new connectn"

服务器-关于java网络编程中获取输入流中数据的问题?

问题描述 关于java网络编程中获取输入流中数据的问题? //服务器端接收消息的类.定制端口号为8888 serviceSocket = new ServerSocket(10000); //获取socket.这个方法是阻塞式的 socket = serviceSocket.accept(); inputStream = socket.getInputStream(); byte buf[] = new byte[1024]; int len = 0; len =inputStream.read

java网络编程中IO数据输入输出阻塞

问题描述 java网络编程中IO数据输入输出阻塞 服务端代码如下: public class Server { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(30000); Socket socket = ss.accept(); PrintStream ps = new PrintStream(socket.getOutputStream()); ps