板凳----Unix 网络编程卷1: 第三章: 套接字编程简介

发布于:2024-07-01 ⋅ 阅读:(29) ⋅ 点赞:(0)

3.1 概述

结构可以在两个方向上传递:从进程到内核和从内核到进程。其中从内核到进程方向的传递是值—结果参数。
地址转换函数在地址的文本表达和它们存放在套接字地址结构中的二进制值之间进行转换。多数现存的IPv4代码使用inet_addr和inet_ntoa这两个函数,不过两个新函数inet_pton
和inet_ntop同时适用于IPv4和IPv6两种代码。

3.2 套接字地址结构

大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义它自己的套接字地址结构。这些结构的名字均以sockaddr_开头。

3.2.1 IPv4 套接字地址结构

IPv4套接字地址结构通常也称为“网际套接字地址结构”,它以sockaddr_in命名,定义在

<netinet/in.h>头文件中。 
struct in_addr { 
in_addr_t 		s_addr; /* 32-bit IPv4 address */ 
/* network byte ordered */ 
}; 
struct sockaddr_in { 
uint8_t		 sin_len; /* length of structure (16) */ 
sa_family_t	 sin_family; /* AF_INET */ 
in_port_t 		sin_port; /* 16-bit TCP or UDP port number */ 
/* network byte ordered */ 
struct in_addr	 sin_addr; /* 32-bit IPv4 address */ 
/* network byte ordered */ 
char sin_zero[8]; /* unused */ 
}; 

从进程到内核传递套接字地址结构的4个套接字函数(bind、connect、sendto和sendmsg)都要调用sockargs函数。该函数从进程复制套接字地址结构,并显式地把它的sin_len字段设置成早先作为参数传递给这4个函数的该地址结构的长度。
从内核到进程传递套接字地址结构的5个套接字函数分别是accept、recvfrom、recvmsg、getpeername和getsockname,均在返回到进程之前设置sin_len字段。
1、长度字段sin_len是为增加对OSI协议的支持而随4.3BSD-Reno添加的。在此之前,第一个成员是sin_family,它是一个无符号短整数(unsigned short)。
2、 即使有长度字段,我们也无须设置和检查它,除非涉及路由套接字

3、POSIX规范只需要这个结构中的3个字段:sin_family、sin_addr和sin_port。
4、 字段s_addr、sin_family和sin_port的POSIX数据类型。
in_addr_t数据类型必须是一个至少32位的无符号整数类型,
in_port_t必须是一个至少16位的无符号整数类型,
sa_family_t可以是任何无符号整数类型。在支持长度字段的实现中,sa_family_t通常是一个8位的无符号整数,而在不支持长度字段的实现中,它则是一个16位的无符号整数。
5、数据类型u_char、u_short、u_int和u_long,它们都是无符号的。
6、IPv4地址和TCP或UDP端口号在套接字地址结构中总是以网络字节序来存储。
7、 32位IPv4地址存在两种不同的访问方法。举例来说,如果serv定义为某个网际套接字地址结构,那么 serv.sin_addr将 按 in_addr结构引用其中的 32 位 IPv4 地址,而serv.sin_addr.s_addr将按in_addr_t(通常是一个无符号的32位整数)引用同一个32位IPv4地址。因此,我们必须正确地使用IPv4地址,尤其是在将它作为函数的参数时,因为编译器对传递结构和传递整数的处理是完全不同的。
8、 sin_zero字段未曾使用,不过在填写这种套接字地址结构时,我们总是把该字段置为0。按照惯例,我们总是在填写前把整个结构置为0,而不是单单把sin_zero字段置为0。
9、套接字地址结构仅在给定主机上使用:虽然结构中的某些字段用在不同主机之间的通信中,但是结构本身并不在主机之间传递。

3.2.2 通用套接字地址结构

当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(也就是以指向该结构的指针)来传递。
<sys/socket.h>头文件中定义一个通用的套接字地址结构。
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family; /* address family: AF_xxx value /
char sa_data[14]; /
protocol-specific address */
};
套接字函数被定义为以指向某个通用套接字地址结构的一个指针作为其参数之一,这正如bind函数的ANSI C函数原型所示:
int bind(int, struct sockaddr , socklen_t);
函数的任何调用都必须要将指向特定于协议的套接字地址结构的指针进行类型强制转换(casting),变成指向某个通用套接字地址结构的指针,例如:
struct sockaddr_in serv; /
IPv4 socket address structure /
/
fill in serv{} */
bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
从内核的角度看,使用指向通用套接字地址结构的指针另有原因:内核必须取调用者的指针,把它类型强制转换为struct sockaddr *类型,然后检查其中sa_family字段的值来确定这个结构的真实类型。
从应用程序开发人员的角度看,要是void *这个指针类型可用那就更简单了,因为无须显式进行类型强制转换。

3.2.3 IPv6 套接字地址结构

IPv6套接字地址结构在<netinet/in.h>头文件中定义。

struct in6_addr { 
unit8_t s6_addr[16]; /* 128-bit IPv6 address */ 
/* network byte ordered */ 
}; 
#define SIN6_LEN /* required for compile-time tests */ 
struct sockaddr_in6 { 
uint8_t 	sin6_len; /* length of this struct (28) */ 
sa_family_t 	sin6_family; /* AF_INET6 */ 
in_port_t	 sin6_port; /* transport layer port# */ 
/* network byte ordered */ 
uint32_t 	sin6_flowinfo; /* flow information, undefined */ 
struct in6_addr 		sin6_addr; /* IPv6 address */ 
/* network byte ordered */ 
uint32_t 		sin6_scope_id; /* set of interfaces for a scope */ 
}; 

1、如果系统支持套接字地址结构中的长度字段,那么SIN6_LEN常值必须定义。
2、IPv6的地址族是AF_INET6,而IPv4的地址族是AF_INET。
3、结构中字段的先后顺序做过编排,使得如果sockaddr_in6结构本身是64位对齐的,那么 128位的sin6_addr字段也是64位对齐的。
4、sin6_flowinfo字段分成两个字段:
■ 低序20位是流标(flow label);
■ 高序12位保留。
5、对于具备范围的地址(scoped address),sin6_scope_id字段标识其范围(scope),最常见的是链路局部地址(link-local address)的接口索引。

3.2.4 新的通用套接字地址结构

结构在<netinet/in.h>头文件中定义。

struct sockaddr_storage { 
uint8_t 	ss_len; /* length of this struct (implementation dependent) */ 
sa_family_t 	ss_family; /* address family: AF_xxx value */ 
/* implementation-dependent elements to provide: 
* a) alignment sufficient to fulfill the alignment requirements of 
* all socket address types that the system supports. 
* b) enough storage to hold any type of socket address that the 
* system supports. 
*/ 
}; 

(1) 如果系统支持的任何套接字地址结构有对齐需要,那么sockaddr_storage能够满足最苛刻的对齐要求。
(2) sockaddr_storage足够大,能够容纳系统支持的任何套接字地址结构。
注意,除了ss_family和ss_len外(如果有的话),sockaddr_storage结构中的其他字段对用户来说是透明的。sockaddr_storage结构必须类型强制转换成或复制到适合于ss_family字段所给出地址类型的套接字地址结构中,才能访问其他字段。

3.2.5 套接字地址结构的比较

5种套接字地址结构进行了比较:IPv4、IPv6、Unix域、数据链路和存储。
假设所有套接字地址结构都包含一个单字节的长度字段,地址族字段也占用1字节,其他所有字段都占用确切的最短长度。
前两种套接字地址结构是固定长度的,而Unix域结构和数据链路结构是可变长度的。为了处理长度可变的结构,当我们把指向某个套接字地址结构的指针作为一个参数传递给某个套接字函数时,也把该结构的长度作为另一个参数传递给这个函数。我们在每种长度固定的结构下
在这里插入图片描述

3.3 值—结果参数

当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针。该结构的长度也作为一个参数来传递,不过其传递方式取决于该结构的传递方向:
是从进程到内核,还是从内核到进程。
(1) 从进程到内核传递套接字地址结构的函数有3个:bind、connect和sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小,例如:
struct sockaddr_in serv;
/* fill in serv{} */
connect(sockfd, (SA *) &serv, sizeof(serv));
既然指针和指针所指内容的大小都传递给了内核,于是内核知道到底需从进程复制多少数据进来。
在这里插入图片描述

套接字地址结构大小的数据类型实际上是socklen_t,而不是int,POSIX规范建议将socklen_t定义为uint32_t。
(2) 从内核到进程传递套接字地址结构的函数有4个:accept、recvfrom、getsockname和getpeername。这4个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针。例如:
struct sockaddr_un cli; /* Unix domain /
socklen_t len;
len = sizeof(cli); /
len is a value */
getpeername(unixfd, (SA ) &cli, &len);
/
len may have changed */
把套接字地址结构大小这个参数从一个整数改为指向某个整数变量的指针,其原因在于:
当函数被调用时,结构大小是一个值(value),它告诉内核该结构的大小,这样内核在写该结构时不至于越界;当函数返回时,结构大小又是一个结果(result),它告诉进程内核在该结构中究竟存储了多少信息。这种类型的参数称为值—结果(value-result)参数。
在这里插入图片描述

使用值—结果参数作为套接字地址结构的长度时,如果套接字地址结构是固定长度的,那么从内核返回的值总是那个固定长度,例如IPv4的sockaddr_in长度是16,IPv6的sockaddr_in6长度是28。然而对于可变长度的套接字地址结构(例如Unix域的sockaddr_un),返回值可能小于该结构的最大长度。
在网络编程中,值—结果参数最常见的例子是所返回套接字地址结构的长度。其他值—结果参数:

  1. select函数中间的3个参数;
  2. getsockopt函数的长度参数;
  3. 使用recvmsg函数时,msghdr结构中的msg_namelen和msg_controllen字段;
  4. ifconf结构中的ifc_len字段;
  5. sysctl函数两个长度参数中的第一个。

3.4 字节排序函数

考虑一个16位整数,它由2字节组成。内存中存储这2字节有两种方法:一种是将低序字节存储在起始地址,这称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,这称为大端(big-endian)字节序。
在这里插入图片描述

#include	"unp.h"

int main(int argc, char **argv){
	union {
	  short  s;
        char   c[sizeof(short)];
           } un;

	un.s = 0x0102;
/*字符串CPU_VENDOR_OS是由GNU的autoconf程序在配置本书中的软件时确定的,它标识CPU类型、厂家和操作系统版本。*/
	printf("%s: ", CPU_VENDOR_OS);
	if (sizeof(short) == 2) {
		if (un.c[0] == 1 && un.c[1] == 2)
			printf("big-endian\n");
		else if (un.c[0] == 2 && un.c[1] == 1)
			printf("little-endian\n");
		else
			printf("unknown\n");
	} else
		printf("sizeof(short) = %d\n", sizeof(short));

	exit(0);
}//~/Unix-Network-Programming/intro/byteorder.c  

wannian07@wannian07-PC:~/Unix-Network-Programming/intro$ make byteorder
gcc -I…/lib -g -O2 -D_REENTRANT -Wall -c -o byteorder.o byteorder.c
byteorder.c: In function ‘main’:
byteorder.c:21:28: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long unsigned int’ [-Wformat=]
printf(“sizeof(short) = %d\n”, sizeof(short));
^
gcc -I…/lib -g -O2 -D_REENTRANT -Wall -o byteorder byteorder.o …/libunp.a -lpthread
(base) wannian07@wannian07-PC:~/Unix-Network-Programming/intro$ ./byteorder
x86_64-unknown-linux-gnu: little-endian

既然网络协议必须指定一个网络字节序(network byte order),发送协议栈和接收协议栈必须就这些多字节字段各个字节的传送顺序达成一致。网际协议使用大端字节序来传送这些多字节整数。由于历史的原因和POSIX规范的规定,套接字地址结构中的某些字段必须按照网络字节序进行维护。因此我们要关注如何在主机字节序和网络字节序之间相互转
换。这两种字节序之间的转换使用以下4个函数。
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
均返回:网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);
均返回:主机字节序的值
在这些函数的名字中,h代表host,n代表network,s代表short,l代表long。short和long这两个称谓是出自4.2BSD的Digital VAX实现的历史产物。并不关心主机字节序和网络字节序的真实值(或为大端,或为小端)。所要做的只是调用适当的函数在主机和网络字节序之间转换某个给定值。在那些与网际协议所用字节序(大端)相同的系统中,这四个函数通常被定义为空宏。
几乎所有的计算机系统都使用8位字节,就用该术语来表示一个8位的量。大多数因特网标准使用八位组(octet)这个术语而不是使用字节来表示8位的量。

3.5 字节操纵函数

操纵多字节字段的函数有两组,它们既不对数据作解释,也不假设数据是以空字符结束的C字符串。当处理套接字地址结构时,我们需要这些类型的函数,因为我们需要操纵诸如IP地址这样的字段,这些字段可能包含值为0的字节,却并不是C字符串。以空字符结尾的C字符串是由在<string.h>头文件中定义、名字以str(表示字符串)开头的函数处理的。
#include <strings.h>
void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
返回:若相等则为0,否则为非0
bzero把目标字节串中指定数目的字节置为0。我们经常使用该函数来把一个套接字地址结构初始化为0。
bcopy将指定数目的字节从源字节串移到目标字节串。
bcmp比较两个任意的字节串,若相同则返回值为0,否则返回值为非0。
我们随后给出ANSI C函数:
#include <string.h>
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src, size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
返回:若相等则为0,否则为<0或>0
memset把目标字节串指定数目的字节置为值c。
memcpy类似bcopy,不过两个指针参数的顺序是相反的。当源字节串与目标字节串重叠时,bcopy能够正确处理,但是memcpy的操作结果却不可知。这种情形下必须改用ANSI C的memmove函数。
记住memcpy两个指针参数顺序的方法之一是记着它们是按照与C中的赋值语句相同的顺序从左到右书写的:dest = src;
记住memset最后两个参数顺序的方法之一是认识到所有ANSI C的memXXX函数都需要一
个长度参数,而且它总是最后一个参数。
memcmp比较两个任意的字节串,若相同则返回0,否则返回一个非0值,是大于0还是小于0则取决于第一个不等的字节:如果ptr1所指字节串中的这个字节大于ptr2所指字节中的对应字节,那么大于0,否则小于0。我们的比较操作是在假设两个不等的字节均为无符号字符(unsigned char)的前提下完成的。

3.6 inet_aton、inet_addr 和 inet_ntoa 函数

(1) inet_aton、inet_addr和inet_ntoa在点分十进制数串(例如“206.168.112.96”)与它长度为32位的网络字节序二进制值间转换IPv4地址。
(2) 两个较新的函数inet_pton和inet_ntop对于IPv4地址和IPv6地址都适用。

#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
返回:若字符串有效则为1,否则为0
in_addr_t inet_addr(const char *strptr);
返回:若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE
char *inet_ntoa(struct in_addr inaddr);
返回:指向一个点分十进制数串的指针
第一个函数inet_aton将strptr所指C字符串转换成一个32位的网络字节序二进制值,并通过指针addrptr来存储。若成功则返回1,否则返回0。
inet_addr进行相同的转换,返回值为32位的网络字节序二进制值。该函数存在一个问题:所有232个可能的二进制值都是有效的IP地址(从0.0.0.0到255.255.255.255),但是当出错时该函数返回 INADDR_NONE常值(通常是一个 32 位均为 1 的值)。这意味着点分十进制数串255.255.255.255(这是IPv4的有限广播地址,见20.2节)不能由该函数处理。新的代码应该改用inet_aton函数。它们对于IPv4地址和IPv6地址都适用。
inet_ntoa函数将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。由该函数的返回值所指向的字符串驻留在静态内存中。这意味着该函数是不可重入的,最后需要留意,该函数以一个结构而不是以指向该结构的一个指针作为其参数

3.7 inet_pton 和 inet_ntop 函数

这两个函数是随IPv6出现的新函数,对于IPv4地址和IPv6地址都适用。函数名中p和n分别代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构中的二进制值。
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
返回:若成功则为1,若输入不是有效的表达格式则为 0,若出错则为-1
const char *inet_ntop(int family, const void *addrptr, char strptr, size_t len);
返回:若成功则为指向结果的指针,若出错则为NULL
这两个函数的family参数既可以是AF_INET,也可以是AF_INET6。如果以不被支持的地址族作为family参数,这两个函数就都返回一个错误,并将errno置为EAFNOSUPPORT。
第一个函数尝试转换由strptr指针所指的字符串,并通过addrptr指针存放二进制结果。若成功则返回值为1,否则如果对所指定的family而言输入的字符串不是有效的表达格式,那么返回值为0。
inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达格式(strptr)。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。
为有助于指定这个大小,在 <netinet/in.h>
头文件中有如下定义:
#define INET_ADDRSTRLEN 16 /
for IPv4 dotted-decimal /
#define INET6_ADDRSTRLEN 46 /
for IPv6 hex string */
如果len太小,不足以容纳表达格式结果(包括结尾的空字符),那么返回一个空指针,并置errno为ENOSPC。
inet_ntop函数的strptr参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小。调用成功时,这个指针就是该函数的返回值。

#include	<sys/types.h>
#include	<sys/socket.h>
#include	<errno.h>
#include	<stdio.h>

#ifndef	INET_ADDRSTRLEN
#define	INET_ADDRSTRLEN		16
#endif

/* include inet_ntop */
const char *
inet_ntop(int family, const void *addrptr, char *strptr, size_t len)
{
	const u_char *p = (const u_char *) addrptr;

	if (family == AF_INET) {
		char	temp[INET_ADDRSTRLEN];

		snprintf(temp, sizeof(temp), "%d.%d.%d.%d",
				 p[0], p[1], p[2], p[3]);
		if (strlen(temp) >= len) {
			errno = ENOSPC;
			return (NULL);
		}
		strcpy(strptr, temp);
		return (strptr);
	}
	errno = EAFNOSUPPORT;
	return (NULL);
}

3.8 sock_ntop 和相关函数

inet_ntop的一个基本问题是:它要求调用者传递一个指向某个二进制地址的指针,而该地址通常包含在一个套接字地址结构中,这就要求调用者必须知道这个结构的格式和地址族。
必须为IPv4编写如下代码:
struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
或为IPv6编写如下代码:
struct sockaddr_in6 addr6;
inet_ntop(AF_INET6, &addr6.sin6_addr, str, sizeof(str));
这就使得我们的代码与协议相关了。
为了解决这个问题,我们将自行编写一个名为sock_ntop的函数,它以指向某个套接字地址结构的指针为参数,查看该结构的内部,然后调用适当的函数返回该地址的表达格式。
#include “unp.h”
char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen);
返回:若成功则为非空指针,若出错则为NULL
这就是本书通篇使用的我们自己定义的函数(非标准系统函数)的说明形式:包围函数原型和返回值的方框是虚线。开头包括的头文件通常是我们自己的unp.h。
sockaddr指向一个长度为addrlen的套接字地址结构。本函数用它自己的静态缓冲区来保存结果,而指向该缓冲区的一个指针就是它的返回值。

#include “unp.h”
int sock_bind_wild(int sockfd, int family);
返回:若成功则为0,若出错则为-1
int sock_cmp_addr(const struct sockaddr *sockaddr1,
const struct sockaddr *
sockaddr2, socklen_t addrlen);
返回:若地址为同一协议族且相同则为0,否则为非0
int sock_cmp_port(const struct sockaddr *sockaddr1,
const struct sockaddr *
sockaddr2, socklen_t addrlen);
返回:若地址为同一协议族且端口相同则为0,否则为非0
int sock_get_port(const struct sockaddr *sockaddr, socklen_t addrlen);
返回:若为IPv4或IPv6地址则为非负端口号,否则为-1
char *sock_ntop_host(const struct sockaddr *sockaddr, socklen_t addrlen);
返回:若成功则为非空指针,若出错则为NULL
void sock_set_addr(const struct sockaddr *sockaddr, socklen_t addrlen, void *ptr);
void sock_set_port(const struct sockaddr *sockaddr, socklen_t addrlen, int port);
void sock_set_wild(struct sockaddr *sockaddr, socklen_t addrlen);
sock_bind_wild将通配地址和一个临时端口捆绑到一个套接字。
sock_cmp_addr比较两个套接字地址结构的地址部分;
sock_cmp_port则比较两个套接字地址结构的端口号部分。
sock_get_port只返回端口号。
sock_ntop_host把一个套接字地址结构中的主机部分转换成表达格式(不包括端口号)。
sock_set_addr把一个套接字地址结构中的地址部分置为ptr指针所指的值;
sock_set_port则只设置一个套接字地址结构的端口号部分。
sock_set_wild把一个套接字地址结构中的地址部分置为通配地址。为那些返回值不是void的上述函数提供了包裹函数,它们的名字以S开头,我们的程序通常调用这些包裹函数。

3.9 readn、writen 和 readline 函数

字节流套接字(例如TCP套接字)上的read和write函数所表现的行为不同于通常的文件I/O。字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。这个现象的原因在于内核中用于套接字的缓冲区可能已达到了极限。此时所需的是调用者再次调用read或write函数,以输入或输出剩余的字节。有些版本的Unix在往一个管道中写多于4096字节的数据时也会表现出这样的行为。这个现象在读一个字节流套接字时很常见,但是在写一个字节流套接字时只能在该套接字为非阻塞的前提下才出现。尽管如此,为预防万一,不让实现返回一个不足的字节计数值,我们总是改为调用writen函数来取代write函数。
我们提供的以下3个函数是每当我们读或写一个字节流套接字时总要使用的函数。

#include "unp.h" 
ssize_t readn(int filedes, void *buff, size_t nbytes); 
ssize_t written(int filedes, const void *buff, size_t nbytes); 
ssize_t readline(int filedes, void *buff, size_t maxlen); 
均返回:读或写的字节数,若出错则为-1
#include	"unp.h"

ssize_t						/* Write "n" bytes to a descriptor. */
writen(int fd, const void *vptr, size_t n)
{
	size_t		nleft;
	ssize_t		nwritten;
	const char	*ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
			if (errno == EINTR)
				nwritten = 0;		/* and call write() again */
			else
				return(-1);			/* error */
		}

		nleft -= nwritten;
		ptr   += nwritten;
	}
	return(n);
}
/* end writen */

void
Writen(int fd, void *ptr, size_t nbytes)
{
	if (writen(fd, ptr, nbytes) != nbytes)
		err_sys("writen error");
}
#include	"unp.h"

static ssize_t
my_read(int fd, char *ptr)
{
	static int	read_cnt = 0;
	static char	*read_ptr;
	static char	read_buf[MAXLINE];

	if (read_cnt <= 0) {
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return(-1);
		} else if (read_cnt == 0)
			return(0);
		read_ptr = read_buf;
	}

	read_cnt--;
	*ptr = *read_ptr++;
	return(1);
}

ssize_t
readline(int fd, void *vptr, size_t maxlen)
{
	int		n, rc;
	char	c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c == '\n')
				break;	/* newline is stored, like fgets() */
		} else if (rc == 0) {
			if (n == 1)
				return(0);	/* EOF, no data read */
			else
				break;		/* EOF, some data was read */
		} else
			return(-1);		/* error, errno set by read() */
	}

	*ptr = 0;	/* null terminate like fgets() */
	return(n);
}
/* end readline */

ssize_t
Readline(int fd, void *ptr, size_t maxlen)
{
	ssize_t		n;

	if ( (n = readline(fd, ptr, maxlen)) < 0)
		err_sys("readline error");
	return(n);
}

良好的防御性编程(defensive programming)技术要求这些程序不仅能够期望它们的对端程序也遵循相同的网络协议,而且能够检查出未预期的网络数据传送并加以修正(恶意企图自然也被检查出来),这样使得网络应用能够从存在问题的网络数据传送中恢复,可能的话还会继续工作。