六星经典CSAPP-笔记(11)网络编程

六星经典CSAPP-笔记(11)网络编程

参照《深入理解计算机系统》简单学习了下Unix/Linux的网络编程基础知识,进一步深入学习Linux网络编程和TCP/IP协议还得参考Stevens的书。

1.网络基础

(略过,待补充)

2.IP地址

2.1 IP地址的表示

IP地址是一个无符号的32位整数。Linux网络程序使用下面这种IP地址结构存储IP地址:

/* Internet address structure */
struct in_addr {
    unsigned int s_addr; /* Network byte order (big-endian) */
};

2.2 IP地址转换

因为网络中的主机有可能有不同的字节序,所以TCP/IP协议定义了一种统一的网络字节序 Network Byte Order,即大尾端字节序(基础知识请参考六星经典CSAPP笔记(2)信息的操作和表示)。这样不论主机是大尾端还是小尾端,网络传输时在数据包的header中保存的IP地址都是网络字节序(大尾端)的,实现了程序的可移植性。为了方便这种操作,Unix提供了一些库函数供我们调用。

System Call Function
unsigned long int htonl(unsigned long int hostlong) Convert a 32-bit integer from host byte order to network byte order
unsigned short int htons(unsigned short int hostshort) Convert a 16-bit integer from host byte order to network byte order
unsigned long int ntohl(unsigned long int netlong) Convert a 32-bit integer from network byte order to host byte order
unsigned short int ntohs(unsigned short int netshort) Convert a 16-bit integer from network byte order to host byte order
int inet_aton(const char *cp, struct in_addr *inp) Convert a dotted-decimal string (cp) to an IP address in network byte order (inp)
char *inet_ntoa(struct in_addr in) Convert an IP address in network byte order to its corresponding dotted-decimal string

前四个函数包含在netinet/in.h头文件中,用来在主机字节序和网络字节序间转换。后两个函数包含在arpa/inet.h中,用来在IP地址字符串和in_addr数据结构间转换。下面来看一个小例子:

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc, char const *argv[])
{
    struct in_addr addr;
    char *addr_str;
    uint32_t net_ip;

    addr_str = "192.168.1.100";

    if (inet_aton(addr_str, &addr) == 0) {
        printf("Convert %s to in_addr failed\n", addr_str);
        exit(1);
    }

    // Convert 192.168.1.100 to host byte order 0x6401a8c0
    printf("Convert %s to host byte order 0x%08x\n", addr_str, addr.s_addr);

    // Convert host byte order 0x6401a8c0 to network byte order 0xc0a80164
    net_ip = htonl(addr.s_addr);
    printf("Convert host byte order 0x%08x "
        "to network byte order 0x%08x\n", addr.s_addr, net_ip);
    addr.s_addr = net_ip;

    return 0;
}

apra/inet.c中的ARPA是什么?CSAPP中讲到了Internet的历史,1957年冷战时埋下了互联网的种子,当时苏联发射了第一颗人造地球卫星Sputnik震惊了世界(村上的小说《斯普特尼克恋人》就是指它)。作为回应,美国政府创建了Advanced Research Projects Agency,即ARPA,想要重新建立科技的领导地位。于是1969年一个全新的网络建立起来,叫做ARPANET,也就是互联网的前身。

通过inet_aton函数将字符串192.168.1.100转换成了in_addr数据结构,其s_addr整数值为0x6401a8c0。简单分析一下:

0x64=100,0x01=1,0xA8=168,0xC0=192

说明我的机器是小尾端字节序。之后再使用htonl函数将小尾端字节序的IP地址转为统一的网络字节序。下面是一道课后练习题,编写一个dd2hex.c小工具,将用户输入的IP地址字符串转成网络字节序的16进制整数。

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>

/**
 * Usage:
 *  unix> gcc dd2hex
 *  unix> ./dd2hex 128.2.194.242
 *  0x8002c2f2
 *
 * @param  argc 2
 * @param  argv 128.2.194.242
 * @return      0x8002c2f2
 */
int main(int argc, char const *argv[])
{
    struct in_addr addr;
    char const *addr_str;
    uint32_t net_ip;

    // 1.Get input arg from cmd line
    if (argc < 1) {
        printf("Usage: dd2hex ip-address-string\n");
        exit(1);
    }
    addr_str = argv[1];

    // 2.Convert to integer in host byte order
    if (inet_aton(addr_str, &addr) == 0) {
        printf("Convert %s to in_addr failed\n", addr_str);
        exit(1);
    }

    // 3.Convert to network byte order
    net_ip = htonl(addr.s_addr);
    printf("0x%08x\n", net_ip);

    return 0;
}

2.网络域名

网络上的主机使用IP地址通信,然而IP地址对于人类是“不友好”的,难于记忆,所以就有了域名Domain Name。最初域名和IP地址的映射关系是保存在文本文件HOSTS.txt中由人工维护的。随后出现了全球性的分布式数据库DNS(Domain Name System),以host entry的形式保存映射关系,Unix中的数据结构是:

/* DNS host entry structure */
struct hostent {
    char *h_name;       /* Official domain name of host */
    char **h_aliases;   /* Null-terminated array of domain names */
    int h_addrtype;     /* Host address type (AF_INET) */
    int h_length;       /* Length of an address, in bytes */
    char **h_addr_list; /* Null-terminated array of in_addr structs */
};

为什么hostent中的h_addr_list是char**而不是struct in_addr**呢?在StackOverflow上的一篇问答中一位老兄给出了解释:

  • hostent是个古老的struct定义,甚至早于void*的出现。在那时,char *被当成万能指针使用。如今理应改成void **而不是char **,但为时已晚!
  • 那时有很多网络协议,作者不知道哪种协议会成为主流。即使在TCP/IP称为主宰的今天,h_addr_list仍然有两种可能,包含struct in_addr *或struct in6_addr *。

网络程序可以从DNS数据库中取出任意的host entry。Unix在netdb.h头文件中提供了API:

System Call Function
struct hostent *gethostbyname(const char *name) Return the host entry associated with the domain name name
struct hostent *gethostbyaddr(const char *addr, int len, 0) Return the host entry associated with the IP address addr

下面是一个获取域名对应信息的小工具,从命令行输入域名或者IP地址,从DNS服务器获取到域名、别名和IP地址的信息。有几个实现上的小细节可以学习一下:

  • 确定输入是域名还是IP:利用inet_aton返回0还是1(转换IP地址字符串到整数是否成功)确定用户输入的到底是域名还是IP地址。
  • 处理错误码:当域名或IP地址无效时,gethostbyaddr/name会返回NULL,这时错误码保存在h_errno中,可以用hstrerr取出对应的错误提示消息。
  • 别名是字符串数组:entry中的h_alias是个字符串数组,因为域名可以对应好多个别名。
  • IP地址是字符串数组:同样,一个域名也可能对应多个IP地址,所以就达到了DNS负载均衡的效果。课后练习题说连续运行hostinfo google.com三次,会看到域名对应的IP地址顺序发生变化。
  • h_addr_list是char* :entry->h_addr_list中的每个值都是char*,使用时被转成了struct in_addr *,具体原因参见前面的解释。
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netdb.h>

/**
 * Usage:
 *  $ ./hostinfo.exe www.baidu.com
 *  Official hostname: www.a.shifen.com
 *  Alias: www.baidu.com
 *  Address: 220.181.112.244
 *  Address: 220.181.111.188
 *
 * @param  argc 2
 * @param  argv www.baidu.com or 220.181.112.244
 * @return      Official hostname/Alias/Address
 */
int main(int argc, char const *argv[])
{
    char **host;
    struct in_addr addr;
    struct hostent *entry;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <domain name or dotted-decimal>\n",
            argv[0]);
        exit(1);
    }

    // Retrieve host entry by "domain name"/"IP address string" from DNS
    if (inet_aton(argv[1], &addr))
        entry = gethostbyaddr((char *)&addr, sizeof(addr), AF_INET);
    else
        entry = gethostbyname(argv[1]);

    if (entry == NULL) {
        fprintf(stderr, "Error: %s\n", hstrerror(h_errno));
        exit(1);
    }

    printf("Official hostname: %s\n", entry->h_name);

    for (host = entry->h_aliases; *host; host++)
        printf("Alias: %s\n", *host);

    for (host = entry->h_addr_list; *host; host++) {
        addr.s_addr = ((struct in_addr *) *host)->s_addr;
        printf("Address: %s\n", inet_ntoa(addr));
    }

    return 0;
}

3.Socket接口

3.1 Socket地址

客户端和服务器通过一条全双工、可靠的连接(full-duplex reliable connection)发送和接收数据流。连接的两端是两个Socket组成的二元组,Socket地址是由IP地址:端口号组成。其中客户端的端口号是由操作系统内核自动分配的临时端口(ephemeral port),而服务器端则是个永久的端口号(well-known port)。

下面来看一下Socket的数据结构,在socket.h中包含了sockaddr,netinet/in.h中包含了sockaddr_in的定义:

/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
    unsigned short sa_family;   /* Protocol family */
    char sa_data[14];           /* Address data. */
};

/* Internet-style socket address structure */
struct sockaddr_in {
    unsigned short sin_family;  /* Address family (always AF_INET) */
    unsigned short sin_port;    /* Port number in network byte order */
    struct in_addr sin_addr;    /* IP address in network byte order */
    unsigned char sin_zero[8];  /* Pad to sizeof(struct sockaddr) */
};

那时的C语言没有void *类型,为了适应不同的网络地址类型,Socket的connect、bind、accept等各种函数都用sockaddr作为参数,应用代码需要将特定协议的struct指针转为通用的sockaddr指针。从上面的struct定义也能看出,sockaddr_in结构的末尾添加了8个字节的padding来匹配sockaddr的长度。

3.2 API纵览

Socket接口是一组与Unix I/O接口配合建立起网络应用的函数集,它在客户端与不同协议实现之间建立起一个抽象层。下面这张图很重要,会作为学习Unix/Linux Socket编程的路线图:

3.3 客户端连接

bind,listen,accept函数都是服务端要使用的,客户端的步骤很简单,主要关注socket和connect两个函数。下面来看一下客户端的代码,主要由以下三步组成:

  1. 创建sockaddr_in:根据IP地址in_addr和端口号组成。
  2. 调用socket():创建套接字,用Unix的描述符表示。
  3. 调用connect():用前两步得到的sockaddr和socket描述符开启连接。
/**
 * Open connection by socket in client-side
 * @param  hostname host name or IP address
 * @param  port     port
 * @return          descriptor
 */
int open_clientfd(char *hostname, int port)
{
    struct hostent *host;
    struct sockaddr_in sockaddr;
    int clientfd;

    // 1.Comine IP address and port as socket address
    memset(&sockaddr, 0, sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(port);

    // 2.Parse IP address from hostname
    if (inet_aton(hostname, &sockaddr.sin_addr) == 0) {
        if ((host = gethostbyname(hostname)) == NULL) {
            fprintf(stderr, "Error: %s\n", hstrerror(h_errno));
            return -1;
        }
        memcpy(&sockaddr.sin_addr.s_addr, host->h_addr_list[0], host->h_length);
    }

    // 3.Create socket
    if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "Error: %s\n", strerror(errno));
        return -2;
    }

    // 4.Establish a connection to server
    if (connect(clientfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) {
        fprintf(stderr, "Error: %s\n", strerror(errno));
        return -3;
    }

    return clientfd;
}

提一下几个实现上的小细节:

  • 内存初始化和拷贝:可以用bzero/bcopy或memset/memcpy。这是两套API,前者是POSIX标准的,后者是C语言标准的。后者包含在memory.h或string.h中。
  • 错误码处理:Socket函数的错误都是放在标准错误码errno中,使用strerror取出错误提示消息,用法与gethostbyname的h_errno和hstrerror完全一样。

3.4 服务端监听

服务端的代码稍微复杂一点点,主要分为四步:

  1. 创建sockaddr_in:使用INADDR_ANY告诉内核此服务器程序能够接受来自任何IP地址的请求。
  2. 调用socket():创建套接字,用Unix的描述符表示。
  3. 调用bind():将前两步得到的sockaddr和socket描述符绑定到一起。
  4. 调用listen():开始监听请求。
/**
 * Listen on socket for incoming request in server-side.
 * @param  port     listen port
 * @return          descriptor
 */
int open_serverfd(int port)
{
    int listenfd, optval = 1;
    struct sockaddr_in sockaddr;

    // 1.Create socket address
    memset(&sockaddr, 0, sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    sockaddr.sin_port = htons(port);

    // 2.Create socket of specific protocal
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "Error: %s\n", strerror(errno));
        return -1;
    }

    // 3.Eliminates "Address already in use" error from bind
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
                    &optval, sizeof(int)) < 0) {
        fprintf(stderr, "Error: %s\n", strerror(errno));
        return -2;
    }

    // 3.Bind socket and address
    if (bind(listenfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) {
        fprintf(stderr, "Error: %s\n", strerror(errno));
        return -3;
    }

    // 4.Start to listen for connection requests with backlog queue 10
    if (listen(listenfd, 10) < 0) {
        fprintf(stderr, "Error: %s\n", strerror(errno));
        return -4;
    }

    return listenfd;
}

3.5 数据通信

有了上面两个工具函数,接下来就能写出完整的客户端和服务器通信代码了。测试方法是先./socket server 7777运行服务端,再./socket client “localhost” 7777 “helloworld”运行客户端。服务端的核心在于启动监听后循环accept()客户请求

// printf, fprintf...
#include <stdio.h>
#include <stdlib.h>
// memset in string/memory.h
#include <string.h>
// strerror
#include <errno.h>
// in_addr/hostent/sockaddr, htons, gethost
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
// sockaddr_in, connect/bind/accept
#include <sys/types.h>
#include <sys/socket.h>
// close, write, read
#include <unistd.h>

// constants
#define MAXLINE 100
#define FAILURE 1

// prototypes
int open_clientfd(char *hostname, int port);
int open_serverfd(int port);

int main(int argc, char const *argv[])
{
    /*
     * 1.Check arguments:
     *      prog server <port> <msg>
     *      prog client <hostname> <port> <msg>
     */
    if (argc <= 2 ||
            ((argc == 3) && strcmp(argv[1], "server")) ||
            (argc == 4) ||
            ((argc == 5) && strcmp(argv[1], "client")) ||
             argc >= 6) {
        fprintf(stderr, "Usage: server <port> or "
                            " client <host> <port> <msg>\n");
        exit(FAILURE);
    }

    if (!strcmp(argv[1], "client")) {
        char *hostname;
        int port;
        int clientfd;
        char *msg;

        hostname = argv[2];
        port = atoi(argv[3]);
        msg = argv[4];

        // 2.Connect to remote server
        if ((clientfd = open_clientfd(hostname, port)) < 0)
            exit(FAILURE);

        // 3.Send messge
        if (send(clientfd, msg, strlen(msg), 0) < 0) {
            fprintf(stderr, "Error: %s\n", strerror(errno));
            exit(FAILURE);
        }

        // 4.Write echo message to stdout
        char buf[MAXLINE];
        int readsize;
        if ((readsize = recv(clientfd, buf, MAXLINE, 0)) > 0)
            puts(buf);

        // 5.Close connection
        close(clientfd);
    }
    else {
        int listenfd, connfd, socklen;
        int port;
        struct sockaddr_in sockaddr;

        port = atoi(argv[2]);

        // 2.Listen for incoming request
        if ((listenfd = open_serverfd(port)) < 0)
            exit(FAILURE);

        while (1) {
            // 3.Accept a new request
            if ((connfd = accept(listenfd, (struct sockaddr *)&sockaddr, &socklen)) < 0) {
                fprintf(stderr, "Error: %s\n", strerror(errno));
                exit(FAILURE);
            }

            printf("New client from %s\n", inet_ntoa(sockaddr.sin_addr));

            // 4.Echo back what client just sent
            char buf[MAXLINE];
            int readsize;
            while ((readsize = recv(connfd, buf, MAXLINE, 0)) > 0)
                write(connfd, buf, strlen(buf));
            memset(buf, 0, strlen(buf));

            // 5.Close connection
            close(connfd);
        }
        close(listenfd);
    }

    return 0;
}

为什么要有listenfd和connfd两个Socket描述符?有什么区别?

listenfd只会创建一次,与服务器生命周期相同;而connfd则是每个客户端与服务器建立起连接时都会创建。直觉上感觉这样做有点复杂,不够简洁,但这样区分开来的好处是:可以轻松构建起能同时处理很多客户端连接的并发服务器。例如,每次accept()后都fork()一个新进程处理。

时间: 05-14

六星经典CSAPP-笔记(11)网络编程的相关文章

六星经典CSAPP笔记系列 - 作者:西代零零发

六星经典CSAPP笔记(1)计算机系统巡游 六星经典CSAPP笔记(2)信息的操作和表示 六星经典CSAPP-笔记(3)程序的机器级表示

六星经典CSAPP笔记(2)信息的操作和表示

2.Representing and Manipulating Information 本章从二进制.字长.字节序,一直讲到布尔代数.位运算,最后无符号.有符号整数.浮点数的表示和运算.诚然有些地方的数学证明有些枯燥,但总体上看,本章还是干货十足的! 2.1 Decimal vs. Binary Notation 我们习惯十进制只是因为我们有十根手指头(?),所以会对二进制感到不习惯.但是二值信号(two-value signal)在表示.存储.传输方面有巨大优势,从打孔带上的有没有孔洞(代码的

六星经典CSAPP-笔记(12)并发编程(上)

六星经典CSAPP-笔记(12)并发编程(上) 1.并发(Concurrency) 我们经常在不知不觉间就说到或使用并发,但从未深入思考并发.我们经常能"遇见"并发,因为并发不仅仅是操作系统内核的"绝招",它也是应用开发中必不可少的技巧: 访问慢I/O设备:就像当应用程序等待I/O中的数据时内核会切换运行其他进程一样,我们的应用也可以用类似的方式,将I/O请求与其他工作重叠从而挖掘并发的潜能. 推迟工作而减少延迟:我们可以推迟一些耗时工作稍后执行,例如内存分配器不在

python学习笔记11 ----网络编程

网络编程 网络编程需要知道的概念 1.网络体系结构就是使用这些用不同媒介连接起来的不同设备和网络系统在不同的应用环境下实现互操作性,并满足各种业务需求的一种粘合剂.网络体系结构解决互质性问题彩是分层方法. 网络(OSI)的7层模型: 应用层--->为应用程序提供网络通信服务 表示层--->数据表示 会话层--->主机间通信(两个应用进程间) 传输层--->端到端的连接,隔离网络的上下层协议,使得网络应用与下层协议无关 网络层--->寻找最优路径,转发数据包 数据链路层---&

六星经典CSAPP-笔记(7)加载与链接(上)

六星经典CSAPP-笔记(7)加载与链接 1.对象文件(Object File) 1.1 文件类型 对象文件有三种形式: 可重定位对象文件(Relocatable object file):包含二进制代码和数据,能与其他可重定位对象文件在编译时合并创建出一个可执行文件. 可执行对象文件(Executable object file):包含可以直接拷贝进行内存执行的二进制代码和数据. 共享对象文件(Shared object file):一种特殊的可重定位对象文件,能在加载时或运行时,装载进内存进

六星经典CSAPP-笔记(10)系统IO

六星经典CSAPP-笔记(10)系统I/O 1.Unix I/O 所有语言的运行时系统都提供了高抽象层次的I/O操作函数.例如,ANSI C在标准I/O库中提供了诸如printf和scanf等I/O缓冲功能的函数:C++中则重载了<<和>>用来支持读写.在Unix系统中,这些高层次的函数基于Unix的系统I/O函数来实现,多数时候我们都无需直接使用底层的Unix I/O.但学习Unix系统I/O能更好地理解一些系统概念,而且当高层次的函数不适用时我们也能轻松地实现想要的功能,例如访

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇

转载请注明出处: ,谢谢! 内容提要 本节主要学习网络通信基础,主要涉及的内容是: TCP/IP协议簇基础:两个模型 IPv4协议基础:IP地址分类与表示,子网掩码等 IP地址转换:点分十进制\二进制 TCP/IP协议簇基础 OSI模型 我们知道计算机网络之中,有各种各样的设备,那么如何实现这些设备的通信呢? 显然是通过标准的通讯协议,但是,整个网络连接的过程相当复杂,包括硬件.软件数据封包与应用程序的互相链接等等,如果想要写一支将联网全部功能都串连在一块的程序,那么当某个小环节出现问题时,整只

nodejs学习笔记之网络编程

了解一下OSI七层模型 OSI层 功能 TCP/IP协议 应用层 文件传输,电子邮件,文件服务,虚拟终端  TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 表示层 数据格式化,代码转换,数据加密 - 会话层 数据格式化,代码转换,数据加密 - 传输层 提供端对端的接口 TCP,UDP 网络层 为数据包选择路由 IP,ICMP,RIP,OSPF,BGP,IGMP 数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,ARP,RARP,MTU 物理层  以二

android开发笔记之网络编程—使用HTTP进行网络编程

上次我们讲到了使用URLConnection的网络编程,URLConnection已经可以非常方便地与指定站点交换信息,URLConnection下还有一个子类:HttpURLConnection. HttpURLConnection在URLConnection的基础上进行改进,增加了一些用于操作HTTP资源的便捷方法. setRequestMethod(String):设置发送请求的方法 getResponseCode():获取服务器的响应代码 getResponseMessage():获取服