《UNIX环境高级编程》笔记 第十六章-网络IPC套接字

本文详细介绍了UNIX环境高级编程中关于网络IPC(Inter-Process Communication)套接字的相关内容,包括套接字描述符、套接字函数(如socket、bind、connect、listen、accept等)、寻址(字节序、地址格式转换)、数据传输(send、recv等)以及套接字选项等。讲解了如何在TCP/IP通信中创建、连接、传输数据和管理套接字,涵盖了面向连接和无连接的通信类型,同时提到了非阻塞I/O和套接字缓冲区的原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 套接字描述符

套接字是通信端点的抽象。如同使用文件描述符访问文件一样,应用程序用套接字描述符访问套接字。其实在UNIX中套接字描述符就是一种文件描述符,许多处理文件描述符的函数(如read、wirte)可以用于处理套接字描述符

套接字通信是双向的。即可以通过一个套接字接收或发送数据

1.1 socket函数

通过socket函数创建一个套接字。成功调用返回的文件描述符将是当前未为进程打开的最低编号的文件描述符。

int socket(int domain, int type, int protocol);

**参数domain(域,地址族):**指定一个通信域,确定通信的特性,包括地址格式。表示各个域的常数都以AF_开头,意为地址族。

描述
AF_INETIPv4因特网域
AF_INET6IPv6因特网域
AF_UNIXUNIX域
AF_UPSPEC未指定(可以代表任何域)

**参数type:**确定套接字类型,进一步确定通信特性。

类型描述
SOCK_DGRAM固定长度的、无连接的、不可靠的报文传递
SOCK_RAWIP协议的数据包接口
SOCK_SEQPACKET固定长度的、有序的、可靠的、面向连接的报文传递
SOCK_STREAM有序的、可靠的、面向连接的字节流
  • SOCK_DGRAM:两个进程之间通信时不需要逻辑连接,只需要向对方所使用的套接字送出一个报文即可。不能保证传递的次序,也无法保证能成功送达。

    面向无连接的 UDP协议是面向报文有边界的报文的协议。发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。

  • SOCK_STREAM:在交换数据之前,本地套接字和与之通信的套接字之间建立一个逻辑连接。SOCK_STREAM提供字节流服务,所以应用程序分辨不出报文的界限。这意味着从SOCK_STREAM套接字读数据时,也许不会返回所有由发送进程所写的字节数。最终可以获得发送过来的所有数据,但也许要通过若干次函数调用才能得到。

    面向连接的TCP协议属于无边界的字节流协议,用户每次调用接收发送函数接口时,不一定都能接收发送一条完整的消息,而是必须对裸字节流进行拆分、组合(与基于有边界报文的UDP协议的应用程序有很大差别)。

  • SOCK_SEQPACKET:和SOCK_STREAM套接字很类似,只是从该套接字得到的是基于报文的服务而不是字节流服务。因此从该套接字接收的数据量与对方所发送的一致。

  • SOCK_RAW:提供了一个接口,用于直接访问下面的网络层(即IP协议)。使用该接口时,应用程序负责构造自己的协议头部(因为传输层协议如TCP/UDP被绕过了)。需要超级用户权限,防止恶意应用程序绕过内建安全机制来创建报文。

**参数protocol:**协议类型。通常为0,因为一般情况下有了前两个参数就可以创建套接字了,操作系统会自动推演出协议类型,这时第三个参数设为0即可。

当同一域和套接字类型支持多个协议时,可以通过protocol参数选择一个特定协议。在AF_INET通信域中,套接字类型SOCK_STREAM的默认协议是TCP;在AF_INET通信域,套接字类型SOCK_DGRAM的默认协议是UDP

在这里插入图片描述

1.2 以套接字描述符作为参数的函数行为

socket函数与调用open类似,都可获得用于I/O的文件描述符。当不再需要该文件描述符时,调用close来关闭对该套接字的访问,并且释放该描述符以便重新使用。但是注意,只有最后一个引用该套接字的描述符被close后,才真正释放该套接字。

虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。有些使用文件描述符作为参数的函数被套接字描述符调用时,函数的行为会有所不同。下图中"未指定和由实现定义"通常意味着该函数对套接字描述符无效。如lseek就不能以套接字描述符作为参数,因为套接字不支持文件偏移量概念

在这里插入图片描述

1.3 shutdown函数

套接字通信是双向的。即可以通过一个套接字接收或发送数据

可以通过shutdown函数禁止一个套接字的I/O。这里与close区别开来,shutdown只是使一个套接字处于不活跃的状态,而非关闭它。且shutdown是作用于套接字的,与引用该套接字的描述符数量无关,不像close那样:close最后一个引用该套接字的描述符才会真正关闭该套接字。

int shutdown(int sockfd, int how);

how参数:该函数具体行为

  • SHUT_WR:关闭写端,无法使用套接字发送数据
  • SHUT_RD:关闭读端,无法使用套接字读数据
  • SHUT_RDWR:即无法读数据,又无法发送数据

2. 寻址

通过两个部分标识一个TCP/IP网络上的通信进程:

  • 计算机IP地址。用于标识网络上我们想与之通信的计算机
  • 端口号。标识该计算机上特定进程。
2.1 字节序

字节序是处理器架构特性,用于指示像整数这样的大数据类型内部的字节如何排序。处理器架构要么支持大端字节序,要么支持小端字节序。

大端法和小端法:

在这里插入图片描述

网络字节序:

网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。(即取高字节的数据存放在低地址)。需要区别的是很多机器是小端的,如X86处理器。

对于TCP/IP应用程序,有4个用来在处理器字节序和网络字节序之间转换的函数

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

其中h表示主机字节序,n表示网络字节序,l表示长整数,s表示短整数。

2.2 地址格式
2.2.1 sockaddr结构

一个地址(IP地址+端口号)标识一个特定通信域的套接字端点。不同通信域(地址族)使用不同的结构体(如sockaddr_in)表示其地址格式,但是传递给函数时为了统一,都需要强制转换成一个通用的地址结构sockaddr

struct sockaddr
{
    sa_family_t sa_family;		/* 地址族 */
    char 		sa_data[14];	/* 地址数据(包括IP地址和端口号) */
};

对于IPv4因特网域(AF_INET),套接字地址用结构sockaddr_in表示,在传递给函数时将sockaddr_in指针类型强制转换为sockaddr指针类型。

struct sockaddr_in
{
    sa_family_t 	sa_family;	/* 地址族 */
    in_port_t 		sin_port;	/* 端口号 */
    struct in_addr 	sin_addr;	/* IPv4地址  */
    char            sin_zero[8];/* 不使用,一般用0填充 */
};
struct in_addr
{
    in_addr_t 		s_addr;		/* IPv4地址 */
};

注意,其中sin_zero为填充字段,应该全部被置0。

对于IPv6因特网域(AF_INET6),套接字地址用结构sockaddr_in6表示,在传递给函数时将sockaddr_in6指针类型强制转换为sockaddr指针类型。

struct sockaddr_in6
{
    sa_family_t 	sin6_family;	/* 地址族 */
    in_port_t 		sin6_port;		/* 端口号 */
    uint32_t 		sin6_flowinfo;	/* IPv6流信息 */
    struct in6_addr sin6_addr;		/* IPv6地址 */
    uint32_t 		sin6_scope_id;	/* IPv6接口范围ID */
};
struct in6_addr {
    uint8_t  		s6_addr[16]		/* IPv6地址 */
};
2.2.2 二进制网络字节序与点分十进制字符串(如"127.0.0.1")之间的相互转换

inet_addr、inet_ntoa、inet_aton函数可用于二进制地址格式与点分十进制字符串相互转换,但是缺陷是这三个函数只适用于IPv4地址

inet_addr函数

若输入字符串有效(点分十进制,如"127.0.0.1")则将字符串转换为32位二进制网络字节序的IPV4地址并返回(即返回输入字符串对应的32位数),否则返回INADDR_NONE(通常是-1)。

但是由于-1也可以表示"255.255.255.255",因此不建议用这个函数。

in_addr_t inet_addr(const char* strptr);
/* 示例 */
struct sockaddr_in mysock;
mysock.sin_addr.s_addr = inet_addr("192.168.1.0");  //设置地址

inet_ntoa函数

将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址

char *inet_ntoa(struct in_addr in);

inet_aton函数(推荐)

将一个字符串IP点分十进制地址转换为一个32位的网络序列IP地址。如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零。

int inet_aton(const char *string, struct in_addr*addr);
/* 示例 */
struct sockaddr_in adr_inet;
inet_aton("127.0.0.1", &adr_inet.sin_addr);

参数:

  • const char *string:传入的点分十进制字符串,如"127.0.0.1"
  • struct in_addr*addr:第一个参数的转换结果会保存在第二个参数中

返回值:成功返回非零值,否则返回零。

可以使用inet_ntop和inet_pton,它们支持IPv4和IPv6地址

int inet_pton(int domain, const char *src, void *dst);
const char *inet_ntop(int domain, const void *src,char *dst, socklen_t size);

domain参数即为通信域(地址域),仅支持AF_INET(IPv4)和AF_INET6(IPv6)

  • inet_pton将点分十进制字符串换格式转换为网络字节序的二进制地址格式。

    • src保存了点分十进制字符串;
    • dst保存了转换后的二进制数据。如果是AF_INET,则dst要足够大存放32位地址;如果是AF_INET6,则dst要足够大存放128位地址。
  • inet_ntop将网络字节序的二进制地址转换为点分十进制字符串格式。

    • 参数src指定了要转换的网络字节序地址二进制数据

    • 参数size指定了缓冲区dst长度,用于保存转换后的点分十进制字符串。可以使用INET_ADDRSTRLEN常量定义一个足够大的空间存放IPv4点分十进制字符串的空间;也可以使用INET6_ADDRSTRLEN常量定义一个足够大的空间存放IPv6点分十进制字符串的空间。

      #define INET_ADDRSTRLEN 16
      #define INET6_ADDRSTRLEN 46
      
2.3 地址查询

提供了一些接口函数访问各种网络配置信息。这些函数返回的网络配置信息被存放在很多地方,可以是静态文件(/etc/hosts和/etc/services)中,也可以由名字服务管理,如DNS。无论这些信息存放在何处,都可以用相同的函数访问它们。

2.3.1 一些知识点

/etc/hosts文件(主机名查询静态表)

hosts文件是Linux系统上一个负责ip地址与域名快速解析的文件,以ascii格式保存在/etc/目录下。hosts文件包含了ip地址与主机名之间的映射,还包括主机的别名。在没有域名解析服务器(DNS)的情况下,系统上的所有网络程序都通过查询该文件来解析对应于某个主机名的ip地址,否则就需要使用dns服务程序来解决。通过可以将常用的域名和ip地址映射加入到hosts文件中,实现快速方便的访问。

cat /etc/hosts
127.0.0.1	localhost

# The following lines are desirable for IPv6 capable hosts
::1	localhost	ip6-localhost	ip6-loopback
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
127.0.1.1	localhost.vm	localhost
127.0.1.1	wudi-huaweiyun	wudi-huaweiyun

可以看出每行由三部分构成

ip地址 	主机名/域名 		(主机别名)

主机名(hostname)和域名(Domain)的区别在于,主机名通常在局域网内使用,通过hosts文件,主机名就被解析到对应ip。域名通常在internet上使用,但如果本机不想使用internet上的域名解析,这时就可以更改hosts文件,加入自己的域名解析。

作为普通用户,你可以查看此文件,因为文件一般都是可读的。要编辑此文件,你需要有 root 权限。


/etc/services文件

/etc/services文件是记录网络服务名和它们对应使用的端口号及协议。一般情况下,不要修改该文件的内容,否则可能会造成端口冲突。

如inetd守护进程就使用该文件。inetd会查看这些细节,以便在数据包到达各自的端口或服务有需求时,它会调用特定的程序。如果每一个服务都在此文件里标注自己所使用的端口信息,则主机上各服务间对端口的使用,将会非常清晰明了,易于管理。

作为普通用户,你可以查看此文件,因为文件一般都是可读的。要编辑此文件,你需要有 root 权限。

$ cat /etc/services 
ssh             22/tcp                          # The Secure Shell (SSH) Protocol
telnet          23/tcp
http            80/tcp          www www-http    # WorldWideWeb HTTP
domain          53/tcp                          # name-domain server
mysql           3306/tcp                        # MySQL
...

每一行的格式如下:

服务名			  端口号/协议	   服务别名			# 注释
2.3.2 gethostent函数

获取计算机系统的主机信息。与/etc/host.conf、/etc/hosts、/etc/nsswitch.conf文件有关

struct hostent *gethostent(void);
void sethostent(int stayopen);
void endhostent(void);

其get、set、end接口与第六章中访问其他数据文件的接口类似。

gethostent

如果数据文件没有打开,gethostent函数打开它。gethostent函数返回文件中的下一个条目

sethostent

如果尚未打开则打开数据文件,并将读写指针设置为该文件初始位置

endhostent

关闭数据文件

gethostent函数每次返回一个指向hostent结构的指针,该结构可能是函数内部的静态变量,因此每次调用gethostent都会覆盖其值。hostent结构体至少包含以下字段

struct hostent
{
  char *h_name;			/* 主机规范名.  */
  char **h_aliases;		/* 主机别名数组. 同一 IP 地址可以绑定多个域名,因此除了当前域名还可以指定其他域名 */
  int h_addrtype;		/* 地址类型.ipv4(AF_INET),ipv6(AF_INET6)  */
  int h_length;			/* 地址长度.  */
  char **h_addr_list;	/* 地址列表,对于用户较多的服务器,可能会分配多个 IP 地址给同一域名,利用多个服务器进行均衡负载。IP地址使用二进制网络字节序存储  */
  ...
};

hostent 结构体变量的组成如下图所示:

在这里插入图片描述

还提供根据键进行搜索,返回指定记录项

struct hostent *gethostbyname(const char *name);	//根据主机名(域名)进行搜索
struct hostent *gethostbyaddr(const void *addr,socklen_t len, int type);	//根据IP地址进行搜索,其中type是AF_INET或AF_INET6,addr和len参数指定了保存IP地址的空间及大小
2.3.3 getnetent函数

获得网络名字和网络编号。与 /etc/networks文件(网络数据库文件)有关

struct netent *getnetent(void);
void setnetent(int stayopen);
void endnetent(void);

get、set、end函数含义与gethostent类似。

netent结构体至少包含以下字段

struct netent
{
  char *n_name;			/* 网络名  */
  char **n_aliases;		/* 别名列表.  */
  int n_addrtype;		/* 地址类型.AF_INET或AF_INET6  */
  uint32_t n_net;		/* 网络编号.按照主机字节序存储  */
};

还提供根据键进行搜索,返回指定记录项

struct netent *getnetbyname(const char *name);		// 根据网络名搜索
struct netent *getnetbyaddr(uint32_t net, int type);// 根据网络编号搜索,net 参数必须采用主机字节顺序。
2.3.4 getprotoent函数

在协议名字和协议编号之间进行映射。与/etc/protocols(协议数据库文件)文件有关

struct protoent *getprotoent(void);
void setprotoent(int stayopen);
void endprotoent(void);

struct protoent *getprotobyname(const char *name);//根据协议名搜索
struct protoent *getprotobynumber(int proto);	  //根据协议编号搜索

struct protoent {
    char  *p_name;       /* 协议官方名 */
    char **p_aliases;    /* 协议别名列表 */
    int    p_proto;      /* 协议编号 */
}
2.3.5 getservent函数

每个服务由一个唯一的众所周知的端口号来支持。可以通过以下函数在服务名和端口号进行映射。与/etc/services(服务数据库文件)文件有关

struct servent *getservent(void);
void setservent(int stayopen);
void endservent(void);

struct servent *getservbyname(const char *name, const char *proto);//根据服务名和协议名搜索。如果proto为NULL,则匹配任何协议
struct servent *getservbyport(int port, const char *proto);//根据端口号和协议名搜索。如果proto为NULL,则匹配任何协议

struct servent {
    char  *s_name;       /* 官方服务名 */
    char **s_aliases;    /* 服务别名列表 */
    int    s_port;       /* 端口号 */
    char  *s_proto;      /* 使用的协议 */
}
2.3.6 getaddrinfo函数

将一个主机名和一个服务名映射到一个地址。与/etc/gai.conf文件有关

int getaddrinfo(const char *node, const char *service,const struct addrinfo *hints,struct addrinfo **res);

void freeaddrinfo(struct addrinfo *res);//释放addrinfo结构(根据ai_next字段释放整个链表)

getaddrinfo函数需要提供主机名(可以是一个节点名或点分十进制字符串的主机地址)、服务名,或者两者都提供。如果只提供一个名字,另一个必须是空指针。

getaddrinfo函数返回一个链表结构addrinfo。如果某个服务支持多重网络接口或多重网络协议,那么getaddrinfo可能会返回多个地址,因此addrinfo结构体是链表结构。

struct addrinfo
{
  int ai_flags;			/* 自定义的标志  */
  int ai_family;		/* 地址族  */
  int ai_socktype;		/* 套接字类型  */
  int ai_protocol;		/* 协议  */
  socklen_t ai_addrlen;		/* 地址长度  */
  struct sockaddr *ai_addr;	/* 地址  */
  char *ai_canonname;		/* 主机规范名  */
  struct addrinfo *ai_next;	/* 链表中的下一个对象  */
};

getaddrinfo函数中的hints参数是一个过滤地址的模板,通过其中的ai_flags、ai_family、ai_socktype、ai_protocol字段进行过滤。hints中其他字段必须设置0,指针字段必须是NULL。

ai_flags字段标志说明如下,该标志定义了如何处理地址和名字

在这里插入图片描述

若getaddrinfo错误,不能使用perror或strerror生成错误消息,而是要通过gai_strerror将返回的错误码转换成错误消息

const char *gai_strerror(int errcode);
2.3.7 getnameinfo函数

根据套接字地址(sockaddr)翻译成一个主机名和一个服务名。与/etc/hosts、/etc/nsswitch.conf、/etc/resolv.conf文件有关

int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,char *host, socklen_t hostlen,char *serv, socklen_t servlen, int flags);

addr参数指向的套接字地址被翻译成主机名和服务名,并存放在host和serv参数指向的缓冲区中。

flags参数提供了一些控制翻译的方式

在这里插入图片描述

2.3.8 补充:gethostname函数

返回本地主机的标准主机名

int gethostname(char *name, size_t len);

3. 将套接字与地址关联(bind函数)

对于服务器,需要给一个接收客户端请求的服务器套接字关联上一个众所周知的地址。客户端应有一种方法来发现连接服务器所需要的地址。最简单的方法就是服务器保留一个地址并且注册在/etc/services或者某个名字服务(如DNS)中

通过bind函数来关联地址和套接字

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  • 指定的地址必须是该进程所在的机器地址,不能是其他机器的地址。

  • addr中的地址族字段必须和通过socket创建该套接字时的地址族相同

  • 一般只能把一个套接字绑定到一个给定地址上。尽管有些协议允许多重绑定

  • addr中的端口号字段不小于1024(除非该进程有超级用户权限),且端口号小于等于49151。即端口号范围属于注册端口

    端口号根据范围分为三种

    • Well-Known Ports(即公认端口号)

      它是一些众人皆知著名的端口号,这些端口号固定分配给一些服务,HTTP 服务、 FTP服务等都属于这一类。知名端口号的范围是:0-1023。

    • Registered Ports(即注册端口)

      它是不可以动态调整的端口段,这些端口没有明确定义服务哪些特定的对象。不同的程序可以根据自己的需要自己定义,注册端口号的范围是:1024-49151。比如listen套接字要bind的端口号就从其中选取。

    • Dynamic, private or ephemeral ports(即动态、私有或临时端口号)

      顾名思义,这些端口号是不可以注册的,这一段的端口被用作一些私人的或者定制化的服务,当然也可以用来做动态端口服务,这一段的范围是:49152–65535。比如connect系统调用就会从动态端口号中选取一个用作客户端与服务器的通信端口

    addr中的端口号也可以取INADDR_ANY宏

    INADDR_ANY宏转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思
    比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢?

    如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个套接字进行数据交换,这样岂不是很繁琐?

    所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。

对于已经绑定了地址的套接字,可以通过getsockname函数发现绑定到套接字上的地址

此处绑定了地址的套接字不一定是通过调用bind显式绑定的,也可能是调用connect或listen函数让系统自动选择一个地址绑定到该套接字上。

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

如果套接字已经与对方连接,可以调用getpeername函数来找到对方地址

int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

4. 建立连接

4.1 connect函数(客户端调用)

在处理一个面向连接的网络服务(套接字类型是SOCK_STREAM或SOCK_SEQPACKET),那么在开始数据通信之前,需要在客户端套接字和服务端套接字之间建立连接。

使用connect函数建立连接

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

将文件描述符sockfd引用的套接字连接到addr指定的地址。

  • 如果sockfd引用的套接字没有绑定到一个地址,connect函数会给调用者绑定一个默认地址

  • 注意,如果connect函数失败,则sockfd套接字的状态会变成未定义的。因此connect失败,应用程序应该关闭该套接字。如果要重试,必须使用一个新的套接字

  • connect函数还可以用于无连接的网络服务(SOCK_DGRAM)。如果用SOCK_DGRAM套接字调用connect,传送的报文的目标地址会设置为connect调用中指定的地址。这样每次传送报文时就不需要再提供地址。并且仅能接收来自指定地址的报文

4.2 listen函数(服务端调用)

将指定套接字用于接受其他进程的连接请求

listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。

int listen(int sockfd, int backlog);
  • int sockfd:唯一标识套接字的文件描述符,该参数代指的套接字用于接受其他进程的请求
  • int backlog: 进程所要入队的未完成连接请求数量上限。 如果连接数目达此上限则client 端将收到ECONNREFUSED 的错误
4.3 accept函数

一旦服务器使用了listen函数,所用的套接字就能接收连接请求。使用accept函数获得连接请求并建立连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • int sockfd:通过listen系统调用用于监听连接的套接字描述符
  • struct sockaddr *addr:sockaddr 结构体指针,即为一个连接实体地址,保存了客户端的IP地址和端口号。
  • socklen_t *addrlen:addr参数指向的内存空间的长度。

返回值是一个套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和sockfd套接字有相同的地址族和套接字类型

如果没有连接请求则阻塞。如果sockfd处于非阻塞模式,则返回-1并且errno为EAGAIN或EWOULDBLOCK。服务器可以使用poll、select、epoll来等待一个请求的到来,带有连接请求的套接字以可读方式出现

注意,传给accept的sockfd套接字没有关联到这个连接,而是继续保持可用状态接收其他连接的请求

5. 数据传输

只要建立连接,就可以使用read和write来通过套接字通信。对于无连接类型套接字(SOCK_DGRAM),如果通过connect函数给套接字设置默认对等地址(对方地址),那么也可以使用read和write来进行通信

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

但是read和write函数能实现的功能较为简单,因此可以用以下6个为数据传递而设计的套接字函数。这些函数可以指定选项、从多个客户端收数据包、发送带外数据

5.1 send函数

和write很像,但是可以指定标志来控制如何发送数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

类似于write,使用send时套接字必须已经连接。buf与len参数含义与write一致。

flags参数控制如何发送数据

在这里插入图片描述

注意,send成功返回不代表连接的另一端进程就一定接收了数据,只能说明将数据发送到了网络驱动程序上。

对于支持报文边界的协议,如果尝试发送的单个报文长度超过协议所支持的最大长度,那么send失败,并将errno设为EMSGSIZE。对于字节流协议,send会阻塞直到整个数据传输完成

5.2 sendto函数

与send类似,区别于sendto可以在无连接的套接字上制定一个目标地址

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

对于面向连接的套接字,目标地址被忽略。对于无连接的套接字,除非先调用connect设置了默认目标地址,否则不能用send而是要用sendto。

5.3 sendmsg函数

sendmsg函数指定多重缓冲区传输数据,类似于writev分散写

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
struct msghdr {
    void         *msg_name;       /* 可选地址 */
    socklen_t     msg_namelen;    /* 地址长度(字节) */
    struct iovec *msg_iov;        /* 缓冲区(iovec)数组 */
    size_t        msg_iovlen;     /* msg_iov元素个数 */
    void         *msg_control;    /* 辅助数据 */
    size_t        msg_controllen; /* 辅助数据长度 */
    int           msg_flags;      /* 标志(对于发送端该字段无用,仅对接收端有用) */
};
struct iovec
{
    void *iov_base;	/* Pointer to data.  */
    size_t iov_len;	/* Length of data.  */
};
5.4 recv函数

recv和read类似,但是recv可以指定标志来控制如何接收数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

flags标志如下

在这里插入图片描述

  • MSG_PEEK:查看下一个要读取的数据但是不真正取走它。当再次调用时返回刚才查看的数据
  • MSG_WAITALL:对于SOCK_STREAM套接字,接收的是数据可以比预期的少。MSG_WAITALL标志会阻止这种行为,直到所请求的数据全部返回,recv函数才会返回。对于SOCK_DGRAM和SOCK_SEQPACKET套接字,MSG_WAITALL标志没有改变什么行为,因为这些基于报文的套接字类型一次读取就返回整个报文。
  • 如果发送者已经调用shutdown结束传输,或者发送端已关闭,那么当所有的数据接收完毕后,recv会返回0。

对于recv、recvfrom、recvmsg函数,如果套接字上没有可用的消息,则接收调用将阻塞等待消息到达,除非套接字是非阻塞的(请参阅fcntl函数),在这种情况下,返回值-1,并将外部变量errno设置为EAGAIN或EWOLDBLOCK。

5.5 recvfrom函数

与recv类似,但是可以指定数据来源

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

通常用于无连接的套接字,否则和recv相同。

5.6 recvmsg函数

将接收到的数据送入多个缓冲区,类似于readv分散读,或者想接收辅助数据

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
struct msghdr {
    void         *msg_name;       /* 可选地址 */
    socklen_t     msg_namelen;    /* 地址长度(字节) */
    struct iovec *msg_iov;        /* 缓冲区(iovec)数组 */
    size_t        msg_iovlen;     /* msg_iov元素个数 */
    void         *msg_control;    /* 辅助数据 */
    size_t        msg_controllen; /* 辅助数据长度 */
    int           msg_flags;      /* 标志(接收到的数据的特征) */
};

参数flags用于控制如何接收数据

返回时,msghdr中的msg_flags字段被设为所接收数据的各种特征,其取值如下

在这里插入图片描述

6. 套接字选项

提供了两个套接字选项函数来控制套接字行为。一个函数用于设置选项;另一个函数用于查询选项状态。可以获取或设置以下3种选项。

  • 通用选项,作用于所有套接字类型上
  • 在套接字层次管理的选项,依赖于下层协议的支持
  • 特定于某协议的选项,每个协议独有
6.1 setsockopt函数

设置套接字选项

int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
  • sockfd参数:要应用的套接字

  • level参数:选项所应用的的协议层。

    • 对于通用层次,使用SOL_SOCKET(通用套接字)
    • 否则,level设置成指定协议编号,如IPPROTO_TCP(TCP协议)、IPPROTO_IP(IP协议)
  • optname参数:要设置的选项类型

在这里插入图片描述

其中SO_REUSEADDR可用于端口复用

  • optval和optlen参数:要设置的选项值。optval指向一个结构体或者一个整数。

    有一些选项(由optname参数标识)是on/off开关,如果整数非0则启用选项;如果整数为0则禁止选项。

6.2 getsockopt函数

查看选项的当前值

int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
6.3 设置套接字选项例子:端口复用

资料

默认的情况下,如果一个网络应用程序的一个套接字 绑定了一个端口,这时候,别的套接字就无法使用这个端口

就比如说服务端用于监听的socket绑定了9999端口并与客户端建立了TCP连接,然后服务端主动断开了连接,这时服务端处于TIME_WAIT状态,因此还占用着9999端口号并需要维持2MSL时间(通常是30秒)。在这段时间内该9999端口号不能绑定用于其他socket,比如此时重启该服务端,用于listen的套接字无法绑定至9999端口(因为处于TIME_WAIT状态还被占用着),此时就需要端口复用技术,来让该用于listen的套接字绑定到被占用着的9999端口。

端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。

设置socket的SO_REUSEADDR选项,即可实现端口复用:

int opt = 1;  //设置1端口复用,0不复用
// sockfd为需要端口复用的套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));

需要注意的是,设置端口复用函数要在绑定之前调用,而且只要绑定到同一个端口的所有套接字都得设置复用:

// sockfd_one, sockfd_two都要设置端口复用
// 在sockfd_one绑定bind之前,设置其端口复用
int opt = 1;  //设置1端口复用,0不复用
setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, (const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
 
// 在sockfd_two绑定bind之前,设置其端口复用
opt = 1;
setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,(const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));

端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。同时,这 n 个套接字发送信息都正常,没有问题。但是,这些套接字并不是所有都能读取信息,只有最后一个套接字会正常接收数据。

但是上面的这种用法并没有什么实际意义。端口复用最常用的用途应该是防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。这种情况下如果设定了端口复用,则新启动的服务器进程可以直接绑定端口。如果没有设定端口复用,绑定会失败,提示ADDR已经在使用中——那只好等等再重试了,麻烦!

7. 带外数据(out-of-band data)

带外数据是一些通信协议所支持的可选功能,用于迅速告知对方本端发生的重要的事件。

它相比于普通的数据(带内数据)拥有更高优先级的数据传输。不论发送缓冲区中是否有排队等待发送的数据,它总是被立即发送。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。

TCP支持带外数据,UDP不支持。TCP将带外数据称为紧急数据。TCP只支持一个字节的紧急数据为了产生带外数据,可以在send、sendto、sendmsg函数中设置MSG_OOB标志,如果发送的字节数超过1个,则最后一个传输的字节将被视为带外数据。如果在接收当前紧急数据字节之前又有新的紧急数据到来,那么已有的字节会被丢弃。

当接收到带外数据时,会向该套接字所有者(进程或进程组)发送SIGURG信号。可以通过fcntl设置指定进程来接收该套接字收到带外数据而发送的SIGURG信号(如果不设置套接字所有者,不会发送信号SIGURG)。

fcntl(sockfd,F_SETOWN,pid);//设置进程pid接收sockfd收到紧急数据时发出的SIGURG信号
pid = fcntl(sockfd,F_GETOWN,0);//获取接收sockfd收到紧急数据时发出的SIGURG信号的进程pid
  • F_GETOWN:**获取该套接字所有权。**获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID

  • F_SETOWN:**设置该套接字所有权。**设置接收SIGIO和SIGURG信号的进程ID或进程组ID

如果fcntl的第三个参数是正值,那么它指定的就是进程ID。如果为非-1的负数,那么它代表的就是进程组ID

TCP支持紧急标记,即在普通数据流中标记紧急数据所在位置。如果采用SO_OOBINLINE套接字选项,那么可以在普通数据中接收紧急数据。为判断是否已经到达紧急标记,使用sockatmark函数

int sockatmark(int sockfd);

当下一个要读取的字节在紧急标记处,sockatmark返回1,否则返回0。

如果使用多路转接函数(select、poll、epoll),当收到带外数据时会满足该套接字描述符异常条件

8. 非阻塞和异步I/O

8.1 非阻塞

通常recv等函数在没有数据可读时会阻塞等待;当套接字输出队列没有足够空间发送消息时,send等函数也会阻塞。

但是在套接字非阻塞模式下,函数不会阻塞而是失败。errno设为EAGAIN或EWOULDBLOCK

通过fcntl函数设置套接字非阻塞

int flag = fcntl( fd, F_GETFL );//获取文件状态标志
flag |= O_NONBLOCK;
fcntl( fd, F_SETFL, flag );//设置为非阻塞套接字

为了不阻塞等待,可以使用select、poll、epoll函数,判断能否非阻塞的读取或发送数据。

8.2 异步I/O

https://kernel.blog.csdn.net/article/details/46334337

除了十四章介绍的通用异步I/O机制(AIO),也可以使用套接字机制自己的异步I/O方式

当网络套接字可读后,内核通过发送SIGIO信号通知应用进程,于是应用可以开始读取数据;同样当网络套接字可发送数据时,也通过SIGIO信号通知进程。具体需要两个步骤

  • 建立套接字所有权,让信号被传递到指定进程。可以由三种方式
    • fcntl中使用F_SETOWN命令
    • ioctl中使用FIOSETOWN命令
    • ioctl中使用SIOCSPGRP命令
  • 当I/O操作不会阻塞时(能非阻塞的读取或发送数据)发送信号SIGIO。可以由两种方式
    • fcntl中使用F_SETFL命令并启用标志O_ASYNC(异步I/O标志)
    • ioctl中使用FIOASYNC命令

9. 补充:TCP socket缓冲区

转载自https://blog.csdn.net/daaikuaichuan/article/details/83061726

TCP拥塞控制:https://www.cnblogs.com/kubidemanong/p/9987810.html

9.1 缓冲区:

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
  read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
在这里插入图片描述

  • I/O缓冲区在每个TCP套接字中单独存在;
  • I/O缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  • 关闭套接字将丢失输入缓冲区中的数据。
9.2 socket编程中的write函数
阻塞模式下:
  • 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据

  • 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒

  • 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。直到所有数据被写入缓冲区 write()/send() 才能返回。

非阻塞模式下:
  • send()函数的过程仅仅是将数据拷贝到协议栈的缓冲区而已,如果缓冲区可用空间不够,则尽可能拷贝,返回成功拷贝的大小;如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN。
9.3 socket编程中的read函数

https://www.cnblogs.com/guozht/p/11236199.html

阻塞模式下:
  • 如果没有发现数据在网络缓冲中会一直等待

  • 当发现有数据的时候会把数据读到用户指定的缓冲区,但是如果这个时候读到的数据量比较少,比参数中指定的长度要小,read 并不会一直等待下去,而是立刻返回。

  • read 的原则:是数据在不超过指定的长度的时候有多少读多少,没有数据就会一直等待。

  • 所以一般情况下:我们读取数据都需要采用循环读的方式读取数据,因为一次read 完毕不能保证读到我们需要长度的数据,

  • read 完一次需要判断读到的数据长度再决定是否还需要再次读取。

非阻塞模式下:
  • 如果发现没有数据就直接返回,

  • 如果发现有数据那么也是采用有多少读多少的进行处理.

  • 所以::read 完一次需要判断读到的数据长度再决定是否还需要再次读取。

9.4 socket编程中read和write函数返回值

read 函数返回值:

  • 大于0:成功读取的数据长度(Byte)

  • 等于0:该 socket 已经被对方关闭

  • 等于-1:异常发生,包括但不限于以下几种:

    • 超时,errno=11
    • errno=EINTR,说明收到了信号发生中断(如去执行了中断处理函数)
    • 连接异常关闭(RST),errno=104
    • 主动关闭socket后再去 read,errno=9
    • 非阻塞模式下的没有数据时,errno=EAGAIN

write 函数返回值:

  • 大于0:成功写入的数据长度(Byte)

  • 等于0:写入长度为0。表示当前写缓冲区已满,是正常情况,下次再来写就行了。

  • 小于0:异常发生,包括但不限于以下几种:

    • errno=EINTR,说明收到了信号发生中断(如去执行了中断处理函数)
    • 主动关闭再写数据,errno=9
    • 连接异常关闭(RST)之后再写数据,errno=32

10. 补充:Linux下客户端/服务器编程流程图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值