Linux下的网络编程看这篇就够了

1.网络基本概念

1.1 OSI参考模型及TCP/IP参考模型

OSI 协议参考模型是基于国际标准化组织(ISO)的建议发展起来的,从上到下共分为 7 层:应用层、表示层、会话层、传输层、网络层、数据链路层及物理层。这个7 层的协议模型虽然规定得非常细致和完善,但在实际中却得不到广泛的应用,其重要的原因之一就在于它过于复杂。 但它仍是此后很多协议模型的基础,这种分层架构的思想在很多领域都得到了广泛的应用。

TCP/IP 协议模型从一开始就遵循简单明确的设计思路,它将TCP/IP 的7 层协议模型简化为4 层,从 而更有利于实现和使用。TCP/IP 的协议参考模型和OSI 协议参考模型的对应关系如图所示:

image-20230211232101916

  • 网络接口层:负责将二进制流转换为数据帧并进行发送和接收,要注意的是数据帧是独立的网络信息传输单元。
  • 网络层:负责将数据帧封装成 IP 数据包,并运行必要的路由算法。
  • 传输层:负责端对端之间的通信会话连接与建立,传输协议的选择根据数据传输方式而定。
  • 应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。

1.2 TCP/IP协议族

虽然 TCP/IP 名称只包含了两个协议,但实际上TCP/IP 是一个庞大的协议族,它包括了各个层次上的众多协议,下图列举了各层中一些重要的协议,并给出了各个协议在不同层次中所处的位置:

image-20230211232626446

  • ARP:用于获得同一物理网络中的硬件主机地址。
  • MPLS:多协议标签协议,是很有发展前景的下一代网络协议。
  • IP:负责在主机和网络之间寻址和路由数据包。
  • ICMP:用于发送有关数据包的传送错误的协议。
  • IGMP:被 IP 主机用来向本地多路广播路由器报告主机组成员的协议。
  • TCP:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响 应的应用程序。
  • UDP:提供了无连接通信,且不对传送包进行可靠性保证。适合于一次传输少量数据,可靠性则 由应用层来负责

1.3 TCP和UDP

篇幅关系,在此主要介绍在网络编程中涉及的传输层 TCP 和 UDP 协议。

TCP

同其他任何协议栈一样,TCP 向相邻的高层提供服务。因为 TCP 的上一层就是应用层,因此,TCP 数据传输实 现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用 TCP 并使用 TCP 服务,提供需要准 备发送的数据,用来区分接收数据应用的目的地址和端口号。 通常应用程序通过打开一个 socket 来使用 TCP 服务,TCP 管理到其他 socket 的数据传递。可以说,通过 IP 的源/目的可以惟一地区分网络中两个设备的连接,通过 socket 的源/目的可以惟一地区分网络中两个应用程序的连接。

  • 三次握手

TCP 对话通过三次握手来进行初始化。三次握手的目的是使数据段的发送和接收同步,告诉其他主机其一 次可接收的数据量,并建立虚连接。 下面描述了这三次握手的简单过程。

  1. 初始化主机通过一个同步标志置位的数据段发出会话请求。
  2. 接收主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发送的数据段的起始字节的顺序号、应答并带有将收到的下一个数据段的字节顺序号。
  3. 请求主机再回送一个数据段,并带有确认顺序号和确认号。

image-20230211233227474

TCP 实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报 到达目的地后,接收方的 TCP 实体往回发送一个数据报,其中包含有一个确认序号,它表示希望收到的下 一个数据包的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据包。

  • TCP数据包头

image-20230211234127862

TCP 数据包头的格式如下:

  1. 源端口、目的端口:16 位长。标识出远端和本地的端口号。
  2. 顺序号:32 位长。标识发送的数据报的顺序。
  3. 确认号:32 位长。希望收到的下一个数据包的序列号。
  4. TCP 头长:4 位长。表明 TCP 头中包含多少个 32 位字。
  5. 6 位未用
  6. URG
  7. ACK:ACK 位置 1 表明确认号是合法的。如果 ACK 为 0,那么数据报不包含确认信息,确认字段被省略。
  8. PSH:表示是带有 PUSH 标志的数据。接收方因此请求数据包一到便将其送往应用程序而不必等到缓冲区装满时才传送。
  9. RST:用于复位由于主机崩溃或其他原因而出现的错误连接。还可以用于拒绝非法的数据包或拒绝连接请求。
  10. SYN:用于建立连接。
  11. FIN:用于释放连接。
  12. 窗口大小:16 位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
  13. 校验和:16 位长。是为了确保高可靠性而设置的。它校验头部、数据和伪 TCP 头部之和。
  14. 可选项:0 个或多个 32 位字。包括最大 TCP 载荷,滑动窗口比例以及选择重发数据包等选项。

UDP

UDP 即用户数据报协议,它是一种无连接协议,因此不需要像 TCP 那样通过三次握手来建立一个连接。 同时,一个 UDP 应用可同时作为应用的客户或服务器方。由于 UDP 协议并不需要建立一个明确的连接, 因此建立 UDP 应用要比建立 TCP 应用简单得多。

UDP 协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是在网络质量越来越高的今天,UDP 的应用得到了大大的增强。它比 TCP 协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP 协议。

  • UDP 数据报头

image-20230211235044566

UDP数据报头的格式如下:

  1. 源地址、目的地址:16 位长。标识出远端和本地的端口号。
  2. 数据报的长度:包括报头和数据部分在内的总的字节数。因为报头的长度是固定的,所以该域主要用来计算可变长度的数据部分(又称为数据负载)

怎么选?

  1. 对数据可靠性的要求:对数据要求高可靠性的应用需选择 TCP 协议,如验证、密码字段的传送都是不允许出错的,而对数据的可 靠性要求不那么高的应用可选择 UDP 传送。
  2. 应用的实时性:TCP 协议在传送过程中要使用三次握手、重传确认等手段来保证数据传输的可靠性。使用 TCP 协议会有 较大的时延,因此不适合对实时性要求较高的应用,如 VOIP、视频监控等。相反,UDP 协议则在这些应 用中能发挥很好的作用。
  3. 网络的可靠性:由于 TCP 协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网 络状况不是很好的情况下需选用 TCP 协议(如在广域网等情况),但是若在网络状况很好的情况下(如局 域网等)就不需要再采用 TCP 协议,而建议选择 UDP 协议来减少网络负荷。

1.4 IP地址

  • 数据在互联网上的传输过程

  • 一个网络程序的软硬件组织

  • IP地址结构

一个IP地址就是一个无符号32位整数。网络程序将其存放在如下所示结构体中:

struct in_addr{
    uint32_t s_addr;	//大端法表示的IP地址
};

1.4.1 字节序的转化

计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式, PC 机通常采用小端模式)。Internet 上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对 这两个字节存储优先顺序进行相互转化。

这里用到了 4 个函数:htons()、ntohs()、htonl()和 ntohl()。这 4 个地址分别实现网络字节序和主机字节序的转化,这里的 h 代表 host,n 代表 network,s 代表 short,l 代 表 long。通常 16 位的 IP 端口号用 s 代表,而 IP 地址用 l 来代表:

#include <netinet/in.h>
/* 成功返回要转换的字节序,失败返回-1 */

uint32_t htonl(uint32_t hostlong);		//将主机字节序的32位hostlong转化为32位网络字节序
uint16_t htons(uint16_t hostshort);		//将主机字节序的16位hostshort转化为16位网络字节序

uint32_t ntohl(uint32_t netlong);		//将32位网络字节序的netlong转化为32位主机字节序
uint16_t ntohs(uint16_t netshort);		//将16位网络字节序的netshort转化为16位主机字节序

注意:调用该函数只是使其得到相应的字节序,用户不需清楚该系统的主机字节序和网 络字节序是否真正相等。如果是相同不需要转换的话,该系统的这些函数会定义成空宏。

1.4.2 地址格式的转化

通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制 IPv6 地址),而在通常使用的 socket 编程中所使用的则是二进制值,这就需要将这两个数值进行转换。

这里在 IPv4 中用到的函数有 inet_aton()、inet_addr()和 inet_ntoa(),而 IPv4 和 IPv6 兼容的函数有 inet_pton()和 inet_ntop()。由于 IPv6 是下一代互联网的标准协议,因此,本书讲解的函数都能够同时兼容 IPv4 和 IPv6,但在具体举例时 仍以 IPv4 为例。 这里 inet_pton()函数是将点分十进制地址映射为二进制地址,而 inet_ntop()是将二进制地址映射为点分十进制地址。以上转化函数均位于头文件arpa/inet.h中。

  • inet_addr()
    • 功能:点分十进制地址 ——> 网络字节序IP地址
    • 原型:in_addr_t inet_addr(const char *cp)
    • 返回值:
      • 成功:无符号长整型数
      • 失败:INADDR_NONE
  • inet_aton()
    • 功能:点分十进制地址 ——> 网络字节序IP地址
    • 原型:int inet_aton(const char *cp, struct in_addr *addr)
    • 返回值:
      • 成功:非零
      • 失败:0
  • inet_ntoa()
    • 功能:网络字节序IP地址 ——> 点分十进制地址
    • 原型:char *inet_ntoa(struct in_addr addr)
    • 返回值:
      • 成功:地址字符串指针
      • 失败:NULL
  • inet_ntop()
    • 功能:网络字节序IP地址 ——> 点分十进制地址
    • 原型:const char *inet_ntop(int family,const void *src, char *dst, socklen_t size)
    • 参数:
      • family:AF_INET或AF_INET6,其中AF_INET代表IPv4,AF_INET6代表128位的IPv6地址;
      • src:待转化的网络字节序
      • dst:指向存放转化后的点分十进制IP地址字符串
      • size:存放点分十进制IP地址的长度
    • 返回值:
      • 成功:地址字符串指针
      • 失败:NULL
  • inet_pton()
    • 功能: 点分十进制地址 ——> 网络字节序IP地址
    • 原型:int inet_pton(int family, const char *src, void *dst)
    • 参数:
      • family:AF_INET或AF_INET6
      • src:待转化的点分十进制地址
      • dst:指向存放转化后的网络字节序IP地址
    • 返回值:
      • 成功:1
      • 格式错误:0
      • 失败:-1

1.4.3 域名和IP地址转换

通常,人们在使用过程中都不愿意记忆冗长的 IP 地址,尤其到 IPv6 时,地址长度多达 128 位,那时就更加 不可能一次次记忆那么长的 IP 地址了。因此,使用主机名将会是很好的选择。

为了便于人类的记忆,DNS服务器记录了所有IP地址和主机名的映射,我们可以通过Linux系统下的nslookup程序来查看域名对应的地址。默认本地主机域名localhost总是映射为回送地址(loopback address )127.0.0.1

$ nslookup localhost
Address:127.0.0.1  

命令行下输入hostname会得到本机的IP地址。在通常情况下,多个域名可以映射到同一个或同一组IP地址;

此外,在 Linux 中,有一些函数可以实现主机名和地址的转化,最为常见的有 ==gethostbyname()、gethostbyaddr()、 getaddrinfo()、getnameinfo()==等,它们都可以实现 IPv4 和 IPv6 的地址和主机名之间的转化。以上接口函数均位于<netdb.h>中。

其中 gethostbyname()是将主机名转化为 IP 地址, gethostbyaddr()则是逆操作,是将 IP 地址转化为主机名。

gethostbyname()和 gethostbyaddr()都涉及一个 hostent 的结构体:

struct hostent 
{ 
    char *h_name;		/*正式主机名*/ 
    char **h_aliases;	/*主机别名*/ 
    int h_addrtype;		/*地址类型*/ 
    int h_length;		/*地址字节长度*/ 
    char **h_addr_list;	/*指向 IPv4 或 IPv6 的地址指针数组*/ 
} 

gethostbyname()

  • 原型:struct hostent *gethostbyname(const char *hostname)
  • 参数:
    • hostname:主机名
  • 返回值:
    • 成功:hostent类型指针
    • 失败:-1
  • 备注:调用该函数时可以首先对hostent 结构体中的h_addrtype 和h_length 进行设置,若为IPv4 可设置为AF_INET 和4; 若为 IPv6 可设置为 AF_INET6 和 16;若不设置则默认为 IPv4 地址类型。

另外,getaddrinfo()能实现自动识别 IPv4 地址和 IPv6 地址。它将主机名(网址或点分十进制IP地址)和服务名(端口号)的字符串转化成套接字地址结构。它是可重入和协议无关的,是代替gethostbyname和getservbyname函数的替代品。

getaddrinfo()函数涉及一个 addrinfo 的结构体:

struct addrinfo 
{ 
    int ai_flags;			/*AI_PASSIVE, AI_CANONNAME;*/ 
    int ai_family;			/*地址族*/ 
    int ai_socktype;		/*socket 类型*/ 
    int ai_protocol;		/*协议类型*/ 
    size_t ai_addrlen;		/*地址字节长度*/ 
    char *ai_canonname;		/*主机名*/ 
    struct sockaddr *ai_addr;/*socket 结构体*/ 
    struct addrinfo *ai_next;/*下一个指针链表*/ 
} 
  • getaddrinfo()

    • 原型:int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **result)

    • 参数:

      • host:可以是域名,也可以是点分十进制IP地址。

      • service :可以是服务名(http等),也可是十进制端口号。如果不想把主机名或端口号转换成套接字地址,就把相应的参数设置为NULL,但是至少要有一个有效。

      • hints :控制参数,它是一个特定的addrinfo指针,通过对该addrinfo结构体ai_family、ai_socktype、ai_protocol、ai_flags成员的设置,可以控制getaddrinfo函数返回的套接字地址列表的特性。但其余成员必须设置为0。实际中一般先用memset函数将整个结构清零,然后又选择的对某些成员赋值。

        • ai_family: 可以设置为AF_INET或AF_INET6。前者表示只返回IPv4的地址,后者返回IPv6的地址;当设置为AF_UNSPEC时表示IPv4 或 IPv6 均可。
        • ai_socktype :对host关联的每个地址,该函数默认最多返回3个addrinfo结构,每个的ai_socktype字段不同:“连接”、“数据报”和“原始套接字”。如果ai_socktype被设置为SOCK_STREAM,则表示为字节流套接字 (TCP),则将限制为对每个地址只返回1个连接的addrinfo结构。如果ai_socktype被设置为SOCK_DGRAM,则表示为数据报套接字 (UDP)。
        • ai_flags : 它是一个位掩码,可以进一步修改默认行为,其取值可以是以下宏
          • AI_ADDRCONFIG: 要求只有本地主机被配置为IPv4时,getaddrinfo返回IPv4地址。
          • AI_CANONNAME: 将列表中第一个addrinfo结构的ai_canonname字段指向host的官方名字。
          • AI_NUMERICSERV:强制getaddrinfo的service参数为端口号。
          • AI_PASSIVE: getaddrinfo默认返回用于客户端的主动套接字,如果设置了该标志,那么函数会返回用于服务器的被动套接字,此时host设置为NULL。得到的套接字地址结构中的地址字段sa_data[14]是通配符地址(wildcard address),表示这个服务器会接受所有发送到服务器的IP地址的请求。
        • ai_protocol:
          • IPPROTO_IP:IP 协议
          • IPPROTO_IPV4:IPv4 协议
          • IPPROTO_IPV6:IPv6 协议
          • IPPROTO_UDP:UDP
          • IPPROTO_TCP:TCP
      • result : 指向一个包含sockaddr(套接字地址结构)地址的addrinfo结构体链表。一般调用完这个函数之后,会遍历该链表,依次尝试每个套接字地址,直到socket和connect或bind连接成功。

    • 返回值:

      • 成功:0
      • 失败:-1
    • 备注

      • 通常服务器端在调用 getaddrinfo()之前,ai_flags 设置 AI_PASSIVE,用于 bind() 函数(用于端口和地址的绑定,后面会讲到),主机名 nodename 通常会设置为 NULL。

      • 客户端调用 getaddrinfo()时,ai_flags 一般不设置 AI_PASSIVE,但是主机名 nodename 和服务名 servname(端口)则应该不为空。

      • 即使不设置 ai_flags 为 AI_PASSIVE,取出的地址也可以被绑定,很多程序中 ai_flags 直接设置为 0,即 3 个标志位都不设置,这种情况下只要 hostname 和 servname 设置的没有问题就可以正确绑定。

      • 为了避免内存泄漏,一般在调用完getaddrinfo函数之后,会调用freeaddrinfo函数释放该链表。其函数原型为void freeaddrinfo(struct addrinfo *result)

      • 如果getaddrinfo遇到错误,应用程序可以调用gai_strerror函数将错误代码转换成字符串,其函数原型为const char *gai_strerror(int errcode)

      • 当getaddrinfo创建列表中的addrinfo结构时,会填写除了ai_flags的每个字段。

      • getaddrinfo函数返回的addrinfo结构中的ai_addr指向的套接字地址可以直接用来传递给套接字接口中的函数(socket、connect、bind、listen、accept等),该特点使得我们编写的客户端和服务器能够独立于某个特殊版本的IP协议。下图展示了getaddrinfo返回的数据结构:

  • getnameinfo()

    • 功能:将一个套接字地址结构转化成相应的主机名(网址或点分十进制IP地址)和服务名(端口号)字符串,并将它们复制到host和service缓冲区。它也是可重入和协议无关的,是代替gethostbyaddr和getservbyport函数的替代品。

    • 原型:int getnameinfo( const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags);

    • 参数:

      • sa :指向大小为salen字节的套接字地址结构;

      • host :指向大小为hostlen字节的缓冲区;如果不想要主机名,可以设置为NULL,hostlen设置为0。

      • service :指向大小为servlen字节的缓冲区;如果不想要服务名,也可以设置为NULL,servlen设置为0,但二者不能同时都设为NULL。

      • flags :它是一个位掩码,可以进一步修改默认行为,其取值可以是以下宏的或。

        • NI_NUMERICHOST: getnameinfo函数默认返回域名,若设置此项则返回数字地址。
        • NI_NUMERICSERV: 默认返回服务名(如果可能的话),若设置此项则返回端口号。

1.4.4 各种地址转换的示例

  • 编写函数将它的十六进制参数转换为点分十进制串:
#include <stdio.h>
#include <sys/inet.h>
#include "csapp.h"

int main(int argc, char **argv)
{
    struct in_addr inaddr;
    uint32_t addr;
    char buf[MAXBUF];
	
    if(argc != 2){
        fprintf(stderr, "usage:%s <hex num>\n",argv[0]);
        exit(0);
    }
    sscanf(argv[1],"%x",&addr);
    inaddr.s_addr = htonl(addr);
    
    if(!inet_ntop(AF_INET,&inaddr,buf,MAXBUF))
        unix_error("inet_ntop");
    printf("%s\n",buf);
    exit(0);
}
  • 编写函数将它的点分十进制串参数转换为十六进制:

    #include <stdio.h>
    #include <sys/inet.h>
    #include "csapp.h"
    
    int main(int argc, char **argv)
    {
        struct in_addr inaddr;
        int rc;
        
        if(argc != 2){
            fprintf(stderr, "usage:%s <dotted-decimal>\n",argv[0]);
            exit(0);
        }
        rc = inet_pton(AF_INET, argv[1], &inaddr);
        if(rc == 0)
            app_error("inet_pton error:invalid dotted-decimal address.\n");
        elseif(rc < 0)
            unix_error("inet_pton error.\n");
        
        printf("0x%x\n",ntohl(inaddr.s_addr));
        exit(0);
    }
    
  • getaddrinfo 函数的用法

    #include <stdio.h> 
    #include <stdlib.h> 
    #include <errno.h> 
    #include <string.h> 
    #include <netdb.h> 
    #include <sys/types.h> 
    #include <netinet/in.h> 
    #include <sys/socket.h> 
    int main() 
    { 
        struct addrinfo hints, *res = NULL; 
        int rc; 
    
        memset(&hints, 0, sizeof(hints)); 
        /*设置 addrinfo 结构体中各参数 */ 
        hints.ai_flags = AI_CANONNAME; 
        hints.ai_family = AF_UNSPEC; 
        hints.ai_socktype = SOCK_DGRAM; 
        hints.ai_protocol = IPPROTO_UDP; 
        /*调用 getaddinfo 函数*/ 
        rc = getaddrinfo("localhost", NULL, &hints, &res); 
        if (rc != 0) 
        { 
            perror("getaddrinfo"); 
            exit(1); 
        } 
        else 
        { 
            printf("Host name is %s\n", res->ai_canonname); 
        } 
        exit(0); 
    }
    
  • 当输入一个域名时,得到相应的点分十进制IPv4地址。

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netdb.h>
    
    int main(int argc, char **argv) 
    {
        struct addrinfo *p, *listp, hints;
        char buf[MAXLINE];
        int rc, flags;
    
        if (argc != 2) {
    	fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
    	exit(0);
        }
    
        /* Get a list of addrinfo records */
        memset(&hints, 0, sizeof(struct addrinfo));                         
        hints.ai_family = AF_INET;       /* IPv4 only */        //line:netp:hostinfo:family
        hints.ai_socktype = SOCK_STREAM; /* Connections only */ //line:netp:hostinfo:socktype
        if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
            fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
            exit(1);
        }
    
        /* Walk the list and display each IP address */
        flags = NI_NUMERICHOST; /* Display address string instead of domain name */
        for (p = listp; p; p = p->ai_next) {
            Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
            printf("%s\n", buf);
        } 
    
        /* Clean up */
        Freeaddrinfo(listp);
    
        exit(0);
    }
    
    • 首先,初始化hints结构,使getaddrinfo返回我们想要的地址。我们想得到用作“连接”的IPv4地址,且只想得到域名,不要服务名。

    • 然后,遍历addrinfo结构链表,用getnameinfo将每个套接字地址转换成IPv4地址字符串。

    • 最后,用freeaddrinfo函数释放链表。运行程序,我们会看到twitter.com映射到4个IP地址。

      $ ./hostinfo twitter.com
      199.16.156.102
      199.16.156.230
      199.16.156.6
      199.16.156.70
      

2.套接字接口

在 Linux 中的网络编程是通过 socket 接口来进行的。人们常说的 socket 是一种特殊的 I/O 接口,它也是一 种文件描述符。socket 是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信, 而且通过网络能够在不同机器上的进程之间进行通信。

每一个 socket 都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关 描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket 也有一个类似于打开文件的函数 调用,该函数返回一个整型的 socket 描述符,随后的连接建立、数据传输等操作都是通过 socket 来实现的。

  • 套接字类型
    1. 流式socket(SOCK_STREAM):流式套接字提供可靠的、面向连接的通信流;它使用 TCP 协议,从而保证了数据传输的正确性和顺序性。
    2. 数据报 socket(SOCK_DGRAM):数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可 靠、无差错的。它使用数据报协议 UDP。
    3. 原始 socket: 原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

下图是基于套接口的网络应用概述:

2.1 套接字地址结构

从程序的角度看,套接字就是一个打开文件的描述符。套接字地址存放在以下两种类型的结构体中,其结构定义位于头文件 <netinet/in.h>中,其中sa_family指协议簇,主要包括以下几种:

sa_family

  • AF_INET:IPv4协议
  • AF_INET6:IPv6协议
  • AF_LOCAL:UNIX域协议
  • AF_LINK:链路地址协议
  • AF_KEY:密钥套接字
/*IP套接字地址结构,_in是互联网的缩写*/
struct sockaddr_in{
    uint16_t		sin_family;	//协议簇(AF_INET或AF_INET6)
    uint16_t		sin_port;	//端口号
    struct in_addr 	sin_addr;	//IP地址
    unsigned char	sin_zero[8];//为了与struct sockaddr边界对齐而填充的0字节
}/*通用套接字地址结构*/
struct sockaddr{
    uint16_t		sa_family;	//协议簇(AF_INET或AF_INET6)
    char			sa_data[14];//地址数据
}

为什么会出现两种套接字地址?

网络编程函数connect、bind和accept要求一个指向与协议有关的套接字地址结构的指针。但套接字接口设计者面临的问题是如何定义这些函数,使之能够接受各种类型的套接字地址结构。而当时void *指针还没有发明,所以解决办法是设计的套接字函数都采用通用地址结构作为参数,而所有特定协议的套接字指针在使用时都强制转换成通用结构。

为了简化代码,Steven指导定义:

typedef struct sockaddr SA;

然后,无论何时需要将sockaddr_in结构强制转换成sockaddr结构时,我们都使用(SA)。

2.2 套接字操作函数

socket 编程的基本函数有 socket()、bind()、listen()、accept()、send()、sendto()、recv()以及 recvfrom()等,其 中根据客户端还是服务端,或者根据使用 TCP 协议还是 UDP 协议,这些函数的调用流程都有所区别,这里先对每个函数进行说明,再给出各种情况下使用的流程图。以下接口函数均位于头文件<sys/socket.h>中。

socket():该函数用于建立一个 socket 连接,可指定 socket 类型等信息。在建立了 socket 连接之后, 可对 sockaddr 或 sockaddr_in 结构进行初始化,以保存所建立的 socket 地址信息。

bind():该函数是用于将本地 IP 地址绑定到端口号,若绑定其他 IP 地址则不能成功。另外,它主要用于 TCP 的连接,而在 UDP 的连接中则无必要。

listen():在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新 的连接请求。此时调用 listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。

accept():服务端程序调用 listen()函数创建等待队列之后,调用 accept()函数等待并接收客户端的 连接请求。它通常从由 bind()所创建的等待队列中取出第一个未处理的连接请求。

connect():该函数在 TCP 中是用于 bind()的之后的 client 端,用于与服务器端建立连接,而在 UDP 中由于没有了 bind()函数,因此用 connect()有点类似 bind()函数的作用。

send()和 recv():这两个函数分别用于发送和接收数据,可以用在 TCP 中,也可以用在 UDP 中。 当用在 UDP 时,可以在 connect()函数建立连接之后再用。

sendto()和 recvfrom():这两个函数的作用与 send()和 recv()函数类似,也可以用在 TCP 和 UDP 中。 当用在 TCP 时,后面的几个与地址有关参数不起作用,函数作用等同于 send()和 recv();当用在 UDP 时,可以用在之前没有使用 connect()的情况下,这两个函数可以自动寻找指定地址并进行连 接。

  • socket()

    • 功能:该函数用于建立一个 socket 连接,可指定 socket 类型等信息。在建立了 socket 连接之后, 可对 sockaddr 或 sockaddr_in 结构进行初始化,以保存所建立的 socket 地址信息。
    • 原型:int socket(int family, int type, int protocol)
    • 参数:
      • family: 使用哪种类型的IP地址
        • AF_INET:IPv4 协议
        • AF_INET6:IPv6 协议
        • AF_LOCAL:UNIX 域协议
        • AF_ROUTE:路由套接字(socket)
        • AF_KEY:密钥套接字(socket)
      • type :套接字类型
        • SOCK_STREAM:字节流套接字
        • SOCK_DGRAM:数据报套接字
        • SOCK_RAW:原始套接字
      • protocol: 一般取0(原始套接字除外)
    • 返回值:
      • 成功:非负描述符
      • 失败:-1
    • 备注:最好的方法是利用getaddrinfo函数自动生成这些参数,以后会说。这里函数返回的套接字描述符仅是部分打开的,还不能用于读和写。
  • bind()

    • 功能:该函数是用于将本地 IP 地址绑定到端口号,若绑定其他 IP 地址则不能成功。另外,它主要用于 TCP 的连接,而在 UDP 的连接中则无必要。
    • 原型:int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen)
    • 参数:
      • sockfd:服务器套接字描述符;
      • addr:通用套接字地址,端口号和地址在addr 中给出,若不指定地址,则内核随意分配一个临时端口给该应用程序。
      • addrlen:套接字地址长度,一般取sizeof(sockaddr_in)
    • 返回值:
      • 成功:0
      • 失败:-1
    • 备注:最好的方法也是利用getaddrinfo函数自动生成这些参数。
  • listen()

    • 功能:在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新 的连接请求。此时调用 listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。服务器调用该函数告诉内核,该描述符是被服务器用来监听来自客服端连接请求的。
    • 原型:int listen(int sockfd, int backlog)
    • 参数:
      • sockfd: 需要转化为监听套接字的描述符
      • backlog: 请求队列中允许的最大请求数,大多数系统缺省值为 5 ,一般设为一个较大的值,比如1024。
    • 返回值:
      • 成功:0
      • 失败:-1
  • accept()

    • 功能:服务端程序调用 listen()函数创建等待队列之后,调用 accept()函数等待并接收客户端的连接请求。它通常从由 bind()所创建的等待队列中取出第一个未处理的连接请求。
    • 原型:int accept(int listenfd, struct sockaddr *addr, int *addrlen)
    • 参数:
      • listenfd:监听套接字描述符
      • addr:请求连接的客户端套接字地址
      • addrlen:套接字地址长度
    • 返回值:
      • 成功:非负描述符
      • 失败:-1
    • 备注:监听描述符和已连接描述符的区别
      • 监听描述符作为客户端连接请求的一个端点,通常被创建一次,存在于服务器的整个生命周期;
      • 已连接描述符是客户端与服务器之间已经建立起来的连接的一个端点,服务器每次接受连接请求时都会创建一次,它只存在与服务器为一个客户端服务的过程中。
  • connect()

    • 功能:该函数在 TCP 中是用于 bind()的之后的 client 端,用于与服务器端建立连接,而在 UDP 中由于没有了 bind()函数,因此用 connect()有点类似 bind()函数的作用。
    • 原型:int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen)
    • 参数:
      • clientfd:客户端套接字描述符;
      • addr:通用(服务器端)套接字地址
      • addrlen:套接字地址长度,一般取sizeof(sockaddr_in)
    • 返回值:
      • 成功:0
      • 失败:-1
    • 备注:该函数阻塞等待与服务器的连接,若成功就表示套接字描述符现在可以读写了,并且得到的连接是由(客户端IP地址:客户端分配的临时端口号)唯一表示。最好的方法也是利用getaddrinfo函数自动生成这些参数。
  • send()

    • 原型:int send(int sockfd, const void *msg, int len, int flags)
    • 参数:
      • socktd:套接字描述符
      • msg:指向要发送数据的指针
      • len:数据长度
      • flags:一般为 0
    • 返回值:
      • 成功:发送的字节数
      • 失败:-1
  • recv()

    • 原型:int recv(int sockfd, void *buf,int len, unsigned int flags)
    • 参数:
      • socktd:套接字描述符
      • buf:存放接收数据的缓冲区
      • len:数据长度
      • flags:一般为 0
    • 返回值:
      • 成功:接收的字节数
      • 失败:-1
  • sendto()

    • 原型:int sendto(int sockfd, const void *msg,int len, unsigned int flags, const struct sockaddr *to, int tolen)
    • 参数:
      • socktd:套接字描述符
      • msg:指向要发送数据的指针
      • len:数据长度 flags:一般为 0
      • to:目地机的 IP 地址和端口号信息
      • tolen:地址长度
    • 返回值:
      • 成功:发送的字节数
      • 失败:-1
  • recvfrom()

    • 原型:int recvfrom(int sockfd,void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen)
    • 参数:
      • socktd:套接字描述符
      • buf:存放接收数据的缓冲区
      • len:数据长度
      • flags:一般为 0
      • from:源主机的 IP 地址和端口号信息
      • tolen:地址长度
    • 返回值:
      • 成功:接收的字节数
      • 失败:-1

2.3 套接字编程示例

  • 使用TCP协议套接字编程流程图

    image-20230212170004441

  • 使用UDP协议套接字编程流程图

    image-20230212170053087

  • 编程要求:区分客户端和服务器端两部分,其中服务器端首先建立起 socket,然后与本地端口进行绑定,接着就开始接收从客户端的连接请求并建立与它的连接,并接收客户端发送的消息。客户端则在建立 socket 之后调用 connect()函数来建立连接。

    • 服务端的代码如下所示:

      /*server.c*/ 
      #include <sys/types.h> 
      #include <sys/socket.h> 
      #include <stdio.h> 
      #include <stdlib.h> 
      #include <errno.h> 
      #include <string.h> 
      #include <unistd.h> 
      #include <netinet/in.h> 
      
      #define PORT 4321 
      #define BUFFER_SIZE 1024 
      #define MAX_QUE_CONN_NM 5 
      
      int main() 
      { 
          struct sockaddr_in server_sockaddr,client_sockaddr; 
          int sin_size,recvbytes; 
          int sockfd, client_fd; 
          char buf[BUFFER_SIZE]; 
      
          /*1.建立 socket 连接*/ 
          if ((sockfd = socket(AF_INET,SOCK_STREAM,0))== -1) 
          { 
              perror("socket"); 
              exit(1); 
          } 
          printf("Socket id = %d\n",sockfd); 
      
          /*1.1设置 sockaddr_in 结构体中相关参数*/ 
          server_sockaddr.sin_family = AF_INET; 
          server_sockaddr.sin_port = htons(PORT); 
          server_sockaddr.sin_addr.s_addr = INADDR_ANY; 
          bzero(&(server_sockaddr.sin_zero), 8); 
      
          /*1.2允许重复使用本地地址与套接字进行绑定 */ 
          int i = 1;
          setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
      
          /*2.绑定函数 bind()*/ 
          if (bind(sockfd, (struct sockaddr *)&server_sockaddr, 
                   sizeof(struct sockaddr)) == -1) 
          { 
              perror("bind"); 
              exit(1); 
          } 
          printf("Bind success!\n"); 
          
          /*3.调用 listen()函数,创建未处理请求的队列*/ 
          if (listen(sockfd, MAX_QUE_CONN_NM) == -1) 
          { 
              perror("listen"); 
              exit(1); 
          } 
          printf("Listening....\n"); 
      
          /*4.调用 accept()函数,等待客户端的连接*/ 
          if ((client_fd = accept(sockfd, (struct sockaddr *)&client_sockaddr, &sin_size)) == -1) 
          { 
              perror("accept"); 
              exit(1); 
          } 
      
          /*5.调用 recv()函数接收客户端的请求*/ 
          memset(buf , 0, sizeof(buf)); 
          if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) == -1) 
          { 
              perror("recv"); 
              exit(1); 
          } 
          printf("Received a message: %s\n", buf); 
          
          /*6.关闭套接字*/
          close(sockfd); 
          exit(0); 
      } 
      
    • 客户端的代码如下所示:

      /*client.c*/ 
      #include <stdio.h> 
      #include <stdlib.h> 
      #include <errno.h> 
      #include <string.h> 
      #include <netdb.h> 
      #include <sys/types.h> 
      #include <netinet/in.h> 
      #include <sys/socket.h> 
      
      #define PORT 4321 
      #define BUFFER_SIZE 1024 
      
      int main(int argc, char *argv[]) 
      { 
          int sockfd,sendbytes; 
          char buf[BUFFER_SIZE]; 
          struct hostent *host; 
          struct sockaddr_in serv_addr; 
      
          if(argc < 3) 
          { 
              fprintf(stderr,"USAGE: ./client Hostname(or ip address) Text\n"); 
              exit(1); 
          } 
      
          /*1.地址解析函数*/ 
          if ((host = gethostbyname(argv[1])) == NULL) 
          { 
              perror("gethostbyname"); 
              exit(1); 
          } 
      
          memset(buf, 0, sizeof(buf)); 
          sprintf(buf, "%s", argv[2]); 
      
          /*2.创建 socket*/ 
          if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
          { 
              perror("socket"); 
              exit(1); 
          } 
      
          /*2.1设置 sockaddr_in 结构体中相关参数*/ 
          serv_addr.sin_family = AF_INET; 
          serv_addr.sin_port = htons(PORT); 
          serv_addr.sin_addr = *((struct in_addr *)host->h_addr); 
          bzero(&(serv_addr.sin_zero), 8);
      
          /*3.调用 connect 函数主动发起对服务器端的连接*/ 
          if(connect(sockfd,(struct sockaddr *)&serv_addr, 
                     sizeof(struct sockaddr))== -1) 
          { 
              perror("connect"); 
              exit(1); 
          } 
      
          /*4.发送消息给服务器端*/ 
          if ((sendbytes = send(sockfd, buf, strlen(buf), 0)) == -1) 
          { 
              perror("send"); 
              exit(1); 
          } 
          
          /*5.关闭套接字*/
          close(sockfd); 
          exit(0); 
      } 
      
    • 运行结果:运行时需要先启动服务器端,再启动客户端!

      image-20230212172643652

  • 套接字并发编程

当遇到多个客户端连接服务器端的情况时,由于之前介绍的如 connet()、recv()和 send() 等都是阻塞性函数,如果资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理 I/O 多 路复用的情况了。下面给出两种解决 I/O 多路复用的解决方法,一是利用 fcntl(),二是利用select()函数。

fcntl():针对 socket 编程提供了2种编程特性。

  1. 非阻塞 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_NONBLOCK。
  2. 异步 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_ASYNC。

下面是用 fcntl()将套接字设置为非阻塞 I/O 的实例代码:

/* net_fcntl.c */ 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <sys/wait.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <errno.h> 
#include <string.h> 
#include <sys/un.h> 
#include <sys/time.h> 
#include <sys/ioctl.h> 
#include <unistd.h> 
#include <netinet/in.h> 
#include <fcntl.h> 

#define PORT 1234 
#define MAX_QUE_CONN_NM 5 
#define BUFFER_SIZE 1024 

int main() 
{ 
    struct sockaddr_in server_sockaddr, client_sockaddr; 
    int sin_size, recvbytes, flags; 
    int sockfd, client_fd; 
    char buf[BUFFER_SIZE]; 

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
    { 
        perror("socket"); 
        exit(1); 
    } 
    server_sockaddr.sin_family = AF_INET; 
    server_sockaddr.sin_port = htons(PORT); 
    server_sockaddr.sin_addr.s_addr = INADDR_ANY; 
    bzero(&(server_sockaddr.sin_zero), 8);
    
    /* 允许重复使用本地地址与套接字进行绑定 */ 
    int i = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); 
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr, 
             sizeof(struct sockaddr)) == -1) 
    { 
        perror("bind"); 
        exit(1); 
    } 
    
    if(listen(sockfd,MAX_QUE_CONN_NM) == -1) 
    { 
        perror("listen"); 
        exit(1); 
    } 
    printf("Listening....\n"); 
    
    /* 调用 fcntl()函数给套接字设置非阻塞属性 */ 
    flags = fcntl(sockfd, F_GETFL); 
    if (flags < 0 || fcntl(sockfd, F_SETFL, flags|O_NONBLOCK) < 0) 
    { 
        perror("fcntl"); 
        exit(1); 
    } 

    while(1) 
    { 
        sin_size = sizeof(struct sockaddr_in); 
        if ((client_fd = accept(sockfd, (struct sockaddr*)&client_sockaddr, &sin_size)) < 0) 
        { 
            perror("accept"); 
            exit(1); 
        }
        if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) < 0) 
        { 
            perror("recv"); 
            exit(1); 
        } 
        printf("Received a message: %s\n", buf); 
    } /*while*/ 

    close(client_fd); 
    exit(1); 
} 

运行该程序,结果可以看到,当 accept()的资源不可用(没有任何未处理的等待连接的请求)时,程序就会自动返回:

$ ./net_fcntl 
Listening.... 
accept: Resource temporarily unavailable 

select():使用 fcntl()函数虽然可以实现非阻塞 I/O 或信号驱动 I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的 CPU 资源的占用。在这里可以使用 select()函数来解决这个问题, 同时,使用 select()函数还可以设置等待的时间,可以说功能更加强大。下面是使用 select()函数的服务器端 源代码。客户端程序基本上与 之前例子相同,仅加入一行 sleep()函数,使得客户端进程等待几秒钟才结束。

/* net_select.c */ 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <sys/time.h> 
#include <sys/ioctl.h> 
#include <unistd.h> 
#include <netinet/in.h>

#define PORT 4321 
#define MAX_QUE_CONN_NM 5 
#define MAX_SOCK_FD FD_SETSIZE 
#define BUFFER_SIZE 1024

int main() 
{ 
    struct sockaddr_in server_sockaddr, client_sockaddr; 
    int sin_size, count; 
    fd_set inset, tmp_inset; 
    int sockfd, client_fd, fd; 
    char buf[BUFFER_SIZE]; 

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
    { 
        perror("socket"); 
        exit(1); 
    } 
    server_sockaddr.sin_family = AF_INET; 
    server_sockaddr.sin_port = htons(PORT); 
    server_sockaddr.sin_addr.s_addr = INADDR_ANY; 
    bzero(&(server_sockaddr.sin_zero), 8);
    int i = 1;/* 允许重复使用本地地址与套接字进行绑定 */ 
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); 
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr, sizeof(struct sockaddr)) == -1) 
    { 
        perror("bind"); 
        exit(1); 
    } 

    if(listen(sockfd, MAX_QUE_CONN_NM) == -1) 
    { 
        perror("listen"); 
        exit(1); 
    } 
    printf("listening....\n"); 
    
    /*将调用 socket()函数的描述符作为文件描述符*/ 
    FD_ZERO(&inset); 
    FD_SET(sockfd, &inset); 
    while(1) 
    { 
        tmp_inset = inset; 
        sin_size = sizeof(struct sockaddr_in); 
        memset(buf, 0, sizeof(buf)); 
        /*调用 select()函数*/ 
        if (!(select(MAX_SOCK_FD, &tmp_inset, NULL, NULL, NULL) > 0)) 
        { 
            perror("select"); 
        } 
        for (fd = 0; fd < MAX_SOCK_FD; fd++) 
        { 
            if (FD_ISSET(fd, &tmp_inset) > 0) 
            { 
                if (fd == sockfd) 
                { /* 服务端接收客户端的连接请求 */ 
                    if ((client_fd = accept(sockfd, (struct sockaddr *)&client_sockaddr, &sin_size))== -1) 
                    { 
                        perror("accept"); 
                        exit(1); 
                    } 
                    FD_SET(client_fd, &inset); 
                    printf("New connection from %d(socket)\n", client_fd); 
                } 
                else /* 处理从客户端发来的消息 */ 
                { 
                    if ((count = recv(client_fd, buf, BUFFER_SIZE, 0)) > 0) 
                    { 
                        printf("Received a message from %d: %s\n", client_fd, buf); 
                    } 
                    else 
                    { 
                        close(fd); 
                        FD_CLR(fd, &inset); 
                        printf("Client %d(socket) has left\n", fd); 
                    } 
                } 
            } /* end of if FD_ISSET*/ 
        } /* end of for fd*/ 
    } /* end if while */ 
    close(sockfd); 
    exit(0); 
} 
  • 运行该程序时,可以先启动服务器端,再反复运行客户端程序(这里启动两个客户端进程)即可,服务器端运行结果如下所示:

    image-20230212201027752

3. 套接字接口的辅助函数

套接字接口函数和转换函数看上去有些可怕,相当复杂凌乱。这一节会介绍一些包装函数,它们将会大大简化客户端和服务器通信程序的编写。

3.1 open_clientfd

客户端可以直接利用该函数建立与服务器的连接。

#include "csapp.h"

int open_clientfd(char * hostname, char *port);
				//成功返回套接字描述符,出错返回-1

下面是它的源代码,它是可重入和协议无关的。

int open_clientfd(char *hostname, char *port) {
    int clientfd, rc;
    struct addrinfo hints, *listp, *p;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;  /* Open a connection */
    hints.ai_flags = AI_NUMERICSERV;  /* ... using a numeric port arg. */
    hints.ai_flags |= AI_ADDRCONFIG;  /* Recommended for connections */
    if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
        return -2;
    }
  
    /* Walk the list for one that we can successfully connect to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) 
            continue; /* Socket failed, try the next */

        /* Connect to the server */
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1) 
            break; /* Success */
        if (close(clientfd) < 0) { /* Connect failed, try another */  //line:netp:openclientfd:closefd
            fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
            return -1;
        } 
    } 

    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* All connects failed */
        return -1;
    else    /* The last connect succeeded */
        return clientfd;
}

假设服务器运行在主机hostname上,并在端口port上监听连接请求。

首先,调用getaddrinfo,返回一个addrinfo结构体链表。遍历该列表依次尝试列表中的每个条目中的ai_addr指向的套接字地址,直到调用socket和connect成功。如果一个失败,则在下一次尝试前关闭掉这个套接字描述符。如果成功建立连接,就释放列表内存,并把套接字描述符返回给客户端,客户端就可以利用它与所有Unix I/O函数与服务器通信了。

3.2 open_listenfd

服务器可以直接利用该函数创建一个监听描述符,准备好建立与客户端的连接。

#include "csapp.h"

int open_listenfd(char *port);
				//成功返回套接字描述符,出错返回-1

open_listenfd函数返回一个打开的监听描述符,且已经准备好在端口port上接受客户端的连接请求。下面是它的源代码:

int open_listenfd(char *port) 
{
    struct addrinfo hints, *listp, *p;
    int listenfd, rc, optval=1;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;             /* Accept connections */
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
    hints.ai_flags |= AI_NUMERICSERV;            /* ... using port number */
    if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
        return -2;
    }

    /* Walk the list for one that we can bind to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) 
            continue;  /* Socket failed, try the next */

        /* Eliminates "Address already in use" error from bind */
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,    //line:netp:csapp:setsockopt
                   (const void *)&optval , sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break; /* Success */
        if (close(listenfd) < 0) { /* Bind failed, try the next */
            fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
            return -1;
        }
    }


    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* No address worked */
        return -1;

    /* Make it a listening socket ready to accept connection requests */
    if (listen(listenfd, LISTENQ) < 0) {
        close(listenfd);
	return -1;
    }
    return listenfd;
}

首先,调用getaddrinfo,返回一个addrinfo结构体链表。遍历该列表依次尝试列表中的每个条目中的ai_addr指向的套接字地址,直到调用socket和bind成功。如果失败,则在下一次尝试前关闭掉这个套接字描述符。如果成功绑定,就释放列表内存,并调用listen函数将该套接字描述符转换为监听描述符返回给调用者,服务器就可以利用它与所有Unix I/O函数响应客户端了。

  • 我们使用了setsockopt函数来配置服务器,使得服务器能够被终止、重启和立即接受连接。一个重启的服务器默认将在30秒内拒绝客户端的连接请求。关于setsockopt的使用很复杂,将会专门写篇文章来讲解他的使用方法。
  • 我们使用了AI_PASSIVE标志并将host参数设置为NULL,这样每个套接字地址字段都会被设置为通配符地址,表示服务器接受发送到本机所有IP地址的请求。

3.3 编写客户端echo和服务器程序

3.3.1客户端程序echoclient

客户端首先与服务器建立连接,之后进入循环等待从标准输入读取文本行发送给服务器。再等待从服务器取回回送的行,并输出结果到标准输出。

#include "csapp.h"

int main(int argc, char **argv) 
{
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t rio;

    if (argc != 3) {
	fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
	exit(0);
    }
    host = argv[1];
    port = argv[2];

    clientfd = Open_clientfd(host, port);
    Rio_readinitb(&rio, clientfd);

    while (Fgets(buf, MAXLINE, stdin) != NULL) {
	Rio_writen(clientfd, buf, strlen(buf));
	Rio_readlineb(&rio, buf, MAXLINE);
	Fputs(buf, stdout);
    }
    Close(clientfd); //line:netp:echoclient:close
    exit(0);
}
  • 当fgets遇到EOF或键盘上键入Ctrl + D,或重定向到标准输入的文件中用尽了所有文本行时,循环就终止。
  • 循环终止后,客户端关闭描述符。此时会导致发送一个EOF到服务器。

3.3.2 服务器程序echoserver

服务器首先打开监听描述符,进入循环等待与客户端建立连接,连接之后首先输出客户端的域名和IP,之后调用echo函数为其服务。echo函数返回后关闭已连接的描述符,连接终止。

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv) 
{
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;  /* Enough space for any address */  //line:netp:echoserveri:sockaddrstorage
    char client_hostname[MAXLINE], client_port[MAXLINE];

    if (argc != 2) {
	fprintf(stderr, "usage: %s <port>\n", argv[0]);
	exit(0);
    }

    listenfd = Open_listenfd(argv[1]);
    while (1) {
	clientlen = sizeof(struct sockaddr_storage); 
	connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
    Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE, 
                 client_port, MAXLINE, 0);
    printf("Connected to (%s, %s)\n", client_hostname, client_port);
	echo(connfd);
	Close(connfd);
    }
    exit(0);
}


void echo(int connfd) 
{
    size_t n; 
    char buf[MAXLINE]; 
    rio_t rio;

    Rio_readinitb(&rio, connfd);
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) { //line:netp:echo:eof
	printf("server received %d bytes\n", (int)n);
	Rio_writen(connfd, buf, n);
    }
}
  • clientaddr是一个套接字地址结构,被传递给accept函数,在accept返回前会将连接到的客户端套接字地址填入到clientaddr。
  • 将clientaddr声明为struct sockaddr_storage是因为该结构足够大能装下任何类型的套接字地址,以保持代码的协议无关性。
  • 我们这里建立的echo服务器一次只能处理一个客户端连接。需要不停的在多个客户端间迭代服务,也称之为迭代服务器。
  • echo函数反复读写文本行,直到rio_readlineb函数遇到EOF。

获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值