网络编程笔记

Posted by wsxq2 on 2018-08-01
TAGS:  网络编程TODOnote

说明:本文是《网络应用程序设计》(西安电子科技大学出版社)一书的笔记。其中的图片来自互联网,若侵权请联系wsxq2@qq.com删除。

代码测试环境(uname -a):

  1. 服务器端(Kali Linux):Linux kali 4.15.0-kali3-amd64 #1 SMP Debian 4.15.17-1kali1 (2018-04-25) x86_64 GNU/Linux
  2. 客户端(CentOS 7.4):Linux master 3.10.0-862.3.2.el7.x86_64 #1 SMP Mon May 21 23:36:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

1 网络编程概述

1.1 计算机网络发展

  1. 以单计算机为中心的联机网络
  2. 计算机-计算机网络
  3. 体系结构标准化网络: OSI七层模型、IEEE 802标准等

1.2 OSI参考模型

应用层: 负责向用户提供服务,消息(message), m(t) 表示层: 负责翻译、加密和压缩数据 会话层: 负责对话控制和同步
传输层: 负责报文从一个进程到另一个进程的传递,报文(segment), s(t), TCP 网络层: 负责分组从一个逻辑地址到另一个逻辑地址的传递, 分组(packet), n(t), IP 数据链路层:负责从一跳(节点)到下一跳(节点)的传递, 帧(frame), x(t), 以太网 物理层: 负责从一个节点到另一个节点的传递

1.3 TCP/IP协议簇

概念区分:
服务:相邻层次间下层向上层提供的一组操作
协议:指两个对等实体间同层次上进行通信时使用的规则

应用层:DNS DHCP NFS SNMP TFTP FTP POP SMTP Telnet
传输层:UDP TCP
网络层:IP ICMP IGMP
数据链路层:ARP RARP

1.4 网络编程模式与编程接口

  1. 模式:
    1. C/S模式:
      1. 顾客
      2. 服务器
        1. 重复服务器(iterative server):在同一个时刻只可以响应一个客户端的请求
        2. 并发服务器(concrrent server):在同一个时刻可以响应多个客户端的请求
      3. 服务器提供的服务
    2. B/S模式:
      1. 定义:对C/S结构的一种变化或者改进的结构(增加Web服务器的C/S模式)。用通用浏览器实现了原来需要复杂专用软件才能实现的强大功能,并节约了开发成本,是一种全新的软件系统构造技术。
      2. 结构:三层结构(客户端->Web服务器->后台数据库)
  2. 编程接口:
    1. Unix:socket
    2. Windows:Winsock

1.5 服务方式

  1. 面向连接的服务和无连接的服务
  2. 全双工与半双工连接
  3. 流量控制:使通信双方在收发数据的速度上保持一致
  4. 差错控制:超时重传
  5. 字节流服务:对数据流不提供任何记录边界划分

1.6 编程基础

1.6.1 Linux系统的基本术语

  1. 内核:就是操作系统,包括文件系统、存储管理、CPU调度以及I/O设备管理
  2. 进程:运行时的程序
    1. 用户上下文:用户模式下进程可访问的空间
    2. 正文段:真正可执行的指令
    3. 数据段:程序数据
    4. 堆:可用来动态分配系统进程的数据空间
    5. 内核上下文:只能由内核维护和访问。包含内核跟踪进程运行以及挂起和重新唤醒进程所需要的信息
  3. 系统调用:
    1. 定义:Unix系统提供的可直接使用的内核服务入口的使用
    2. 返回值:
      1. 整型值:
        1. >=0(正确调用)
        2. -1(出错调用,整型的全局变量errno存放错码,错码保存在头文件<errno.h>)
      2. 结构信息:如statfstate返回一个结构指针
  4. 变元表:向执行的程序提供参数。C语言的变元表使用如下:

    1
    2
    3
    4
    5
    6
    7
    
    #include <stdio.h>
       
    int main(int argc, char* argv[]){
        printf("argc: %d\n", argc);
        printf("argv: %s\n", argv[0]);
        return 0;
    }
    
  5. 环境表:向执行的程序提供环境变量。C语言的环境表使用如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #include <stdio.h>
       
    int main(int argc, char* argv[], char* envp[])
    {
    	int i;
    	printf("enviroment parameters: \n");
    	for(i=0; envp[i]!=(char*)0; i++){
    		printf("%s\n", envp[i]);
    	}
    	return 0;
    }
    

    或者使用外部变量environ<unistd.h>中声明了该变量):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    int main(int argc, char * argv[])
    {
        int i;
        extern char ** environ;
        printf("environment parameters: \n");
        for(i=0; environ[i]!=(char *)0; i++){
            printf("%s\n", environ[i]);
        }
        return 0;
    }
    

    此外和环境变量相关的函数有:

    1. getenv(): 访问一个环境变量
    2. setenv(): 在程序里面设置某个环境变量
    3. unsetenv(): 清除某个特定的环境变量
  6. SHELL(外壳):
    1. sh(Bourne shell): 1977年,使用最广泛,在很多Unix版本中,它仍然是root的默认shell。
    2. tcsh: 1983年,目前作为FreeBSD和其延伸发行版的默认shell。
    3. bash(Bourne-Again shell): 1989年,Linux与Mac OS X v10.4都将它作为默认shell。

    详情参见维基百科-壳层

1.6.2 标识符

  1. 进程标识(PID):进程的标识符

    1
    2
    3
    4
    5
    
    int main()
    {
        printf("process pid=%d, father process pid=%d\n", getpid(), getppid());
        return 0;
    }
    

    Linux系统中的每一个进程都包括一个叫做task_struct的数据结构,而所有指向这些数据结构的指针组成系统的一个进程向量数组

  2. 用户标识符(UID):用户的标识符
    1. 相关文件:/etc/passwd
    2. 系统调用:
      1. unsigned short getuid(): 获得进程的用户号
      2. unsigned short geteuid(): 获得进程的有效用户号(有效用户号用于设置用户标识符程序中, 为用户提供一些权限, 导致其和用户号不同)
  3. 组标识(GID):用户组的标识符
    1. 相关文件: /etc/group
    2. 系统调用:
      1. unsigned short getgid(): 获得进程的用户组号
      2. unsigned short getegid(): 获得进程的有效用户组号
  4. 进程组号:若干个进程可以属于同一进程组,进程组号就是用来标识这一组进程的
    1. 成员:具有相同进程组号的进程
    2. 组长:进程组号与其进程号相同的进程
    3. 系统调用:
      1. int getpgrp(int pid): 获得指定进程的进程组号
      2. int setgrp(int pid, int prgp): 修改指定进程的进程组号
  5. 终端组号和控制终端
    1. 终端组号:打开终端的进程组组长的进程号,该进程被称为终端控制进程,每个终端只有一个控制进程
    2. 控制终端:终端组号用来标识一个进程组的控制终端。控制终端可以被名为/dev/ftg的设备自动引用
    3. 系统调用:ioctl为它的控制终端设置或检查终端组号
  6. 超级用户:UID为0的注册名一般是root的用户。具有整个文件系统的访问权,并控制着整个系统的安全

1.6.3 文件

  1. 文件描述符
    1. 定义:内核(kernel)利用文件描述符(file descriptor)来访问文件。 文件描述符是非负整数。 打开现存文件或新建文件时,内核会返回一个文件描述符。 读写文件也需要使用文件描述符来指定待读写的文件。
    2. 系统调用:

      1
      2
      3
      4
      
      #include <sys/stat.h>
      
      int stat(char * patthname, struct stat * buf); //通过文件路径获取文件属性
      int fstat(int fildes, struct stat * buf); //通过文件描述符获取文件属性
      
    3. 示例程序:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <stdio.h>
            
      int main(int argc, char* argv[]) {
          int i;
          char * str;
          struct stat statbuf;
          for(i=0; i<argc; i++){
              printf("%s: ", argv[i]);
              if(stat(argv[i], &statbuf)<0){
                  perror("stat error!");
              }
              switch(statbuf.st_mode&S_IFMT){
                  case S_IFDIR: str="directory"; break;
                  case S_IFCHR: str="character special"; break;
                  case S_IFBLK: str="block special"; break;
                  case S_IFREG: str="regular"; break;
            
                  #ifdef S_IFLNK
                      case S_IFLNK: str="symbolic link"; break;
                  #endif
                  #ifdef S_IFSOCK
                    case S_IFSOCK: str="socket"; break;
                  #endif
                  #ifdef S_IFIFO
                    case S_IFIFO: str="fifo"; break;
                  #endif
                  default: str="unknown mode"; break;
              }
              printf("%s\n", str);
          }
          return 0;
      }
      
  2. 文件存取权限
    1. 相关文件属性:rwxrwxrwx root root(通过ls -l查看)
    2. 允许访问某一文件的条件(满足下面的一条即可):
      1. 进程的有效用户标识为0
      2. 进程的有效用户标识与文件属主的用户标识相一致,且文件属主的访问允许位为“1”
      3. 若进程的有效用户标识与文件属主的用户标识不匹配,而有效用户组号和文件主的组号相匹配,且文件的相应用户组访问允许位为“1”
      4. 若进程的有效用户标识及其有效用户组号均与文件属主相应标识不匹配,但文件的其他用户访问允许位为“1”
  3. 文件存取方式字

    八进制 意义
    04000 执行时设置用户标识符
    02000 执行时设置组标识
    01000 执行后保存文件文本映像
    00400 用户读
    00200 用户写
    00100 用户执行
    00040 用户组读
    00020 用户组写
    00010 用户组执行
    00004 其他用户读
    00002 其他用户写
    00001 其他用户执行
  4. 文件方式初模
    1. 定义:在建立一个新文件或新目录时要用到文件方式初模,它用来指定新文件中的存取方式字中哪些位要清除掉
    2. 系统调用:int umask(int cmask),其中cmask低9位指定该进程的文件方式初模

1.6.4 计算机网络基本术语

  1. 地址:网络地址+主机地址+进程标识(如:192.168.1.100:80)
  2. 连接与相关
    1. 连接:两个进程之间的通信链路
    2. 相关:组成一个连接的两个进程的元素组。如{协议,本地地址,本地进程,外部地址,外部进程}
  3. 带外数据与缓存
    1. 缓存:字节流服务需要缓存,为了安全传送数据、进行流量控制
    2. 带外数据:紧急数据。一般发送方在发送缓存数据前发送带外数据;接收方在处理缓存数据前接收带外数据。如终端中发送CTRL_C
  4. 分组交换
    1. 线路交换网:也称电路交换。如电话系统
    2. 分组交换网:如互联网

2 基于TCP套接字的编程

2.1 概述

ARPA(美国国防高级研究计划局)资助了加利福尼大学伯克利分校一个研究组(该研究组将TCP/IP软件移植到UNIX操作系统中)。作为项目的一部分,为了支持TCP/IP功能增加的新系统调用接口,形成了Berkeley Socket,这个系统被称为Berkeley UNIX或BSD UNIX(TCP/IP首次出现在BSD4.1版本)。由于许多计算机厂商都采用了Berkeley UNIX, Socket得到了迅速普及并被广泛使用。socket已成为网络编程的事实上的标准。

伯克利软件套件(英语:Berkeley Software Distribution,缩写为BSD),也被称为伯克利Unix(Berkeley Unix),是一个操作系统的名称。

Linux提供的套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。

2.2 套接字和套接字地址

2.2.1 套接字

  1. 定义:套接字是两个通信通道上的端节点。套接字函数可以用来产生通信信道,通过信道两个应用程序间可以传送数据
  2. Linux支持的协议簇(family):
    1. UNIX: UNIX域套接字
    2. INET: Internet地址簇(TCP/IP)
    3. ipx: Novell IPX
    4. APPLETALK: Appletalk DDP
    5. X.25: X.25
  3. 传送提供者(protocol):TCP, UDP, XNS
  4. Linux的BSD套接字支持的类型(type):
    1. 流式(stream):提供可靠的双向顺序数据流连接。可以保证数据传输中的完整性、正确性和单一性。TCP支持这种类型的套接字
    2. 数据报(datagram):可以像流式套接字一样提供双向的数据传输,但不能保证传输的数据一定能够到达目的节点,也戴法保证到达数据以正确的顺序到达以及数据的单一性、正确性。UDP支持这种类型的套接字
    3. 原始(raw):允许进程直接存取下层的协议
    4. 可靠递送消息(reliable delivered messages):和数据报套接字一样,但能保证数据的到达
    5. 顺序数据包(sequenced packets):和流式套接字相同,但是它的数据包的大小是固定的
    6. 数据包(packet):是Linux中的一种扩展。允许进程直接存取设备层的数据包

2.2.2 套接字地址

  1. 概述:套接字接口利用传送提供者进行工作,不同的传送提供者有不同的地址,套接字接口允许指定任意类型的地址。Linux系统的套接字是一个通用的网络编程接口,它支持多种协议,每一种协议使用不同的套接字地址结构。Linux系统定义了一种通用的套接字地址结构,可以保持套接字函数调用参数的一致性
  2. 通用的套接字地址结构:
    1
    2
    3
    4
    
    struct sockaddr{
        unsigned short sa_family; //协议标识符,如AF_INET
        char sa_data[14]; //协议地址
    };
    
  3. TCP/IP协议簇的套接字地址也可以采用如下结构:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #include <netinet/in.h> //使用与平台无关的头文件以保持代码的可移植性
    #include <sys/socket.h> //使用与平台无关的头文件以保持代码的可移植性
    struct in_addr{
        _u32 s_addr; //32位IP地址
    };
    struct sockaddr_in{
        short int sin_family; //协议标识符,应设为AF_INET
        unsigned short int sin_port; //必须保证以网络字节顺序传输
        stuct in_addr sin_addr; //必须保证以网络字节顺序传输
        unsigned char sin_zero[8]; //末被使用,只是为了使两个结构体在内存中具有相同的尺寸(sturct sockaddr和struct sockaddr_in),使用时应置0
    };
    

2.2.3 IP地址的使用

需要进行字符串形式的IP地址和二进制形式的地址间的转换。使用如下函数将点分十进制数字形式表示的IP地址与32柆的网络字节顺序的二进制形式的IP地址进行转换:

1
2
3
4
5
6
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);
char * inet_ntoa(struct in_addr in);

2.3 基本套接字函数

  1. socket(), socketpair()
  2. bind(), listen(), accept()connect()
  3. read(), write()

使用man <函数名>以了解详细使用方法,如man socket

  1. socket():
    1
    2
    
    #include <sys/socket.h>
    int socket(int family, int type, int protocol);
    
    1. 返回值:
      1. 成功:文件描述符
      2. 失败:-1
    2. 参数:
      1. family: 表示所用的协议是协议簇中的哪一个。可以为:
        • AF_INET: TCP/IP协议集合
        • AF_UNIX: UNIX域协议簇,在本机的进程间通信时使用
        • AF_ISO: ISO协议簇
      2. type: 表示套接字类型:
        • SOCK_STREAM: 提供虚电路服务的流套接字
        • SOCK_DGRAM: 提供数据报服务的套接字
        • SOCK_RAW: 原始套接字,只对Internet协议有效,可以用来直接访问IP协议
        • SOCK_SEQPACKET: 有序分组套接字
        • SOCK_RDM: 能可靠交付信息的数据报套接字
      3. protocol: 指定所用协议。
        • 0:使用默认协议
        • IPPROTO_TCP
        • IPPROTO_UDP
        • IPPROTO_ICMP
        • IPPROTO_RAW

      常见适应AF_INET协议簇的type及protocol的属性取值见下表:

      family type protocol 结果
      AF_INET SOCK_STREAM 0 或 IPPROTO_TCP TCP
      AF_INET SOCK_DGRAM 0 或 IPPROTO_UDP UDP
      AF_INET SOCK_RAW IPPROTO_ICMP ICMP
      AF_INET SOCK_RAW IPPROTO_RAW IP
  2. socketpair():

    1
    2
    
    #include <sys/socket.h>
    int socketpair(int family, int type, int protocol, int fd_array[2]);
    
    1. 返回值:
      1. 成功:2
      2. 失败:1
    2. 参数:
      1. family: 只能取值AF_UNIX
      2. type: SOCK_STREAM 或 SOCK_DGRAM
      3. protocol: 只能取值0
      4. fd_array[2]: 返回两个套接字描述符(区别于文件描述符),这两个套接字描述符是双向的(区别于管道)
  3. bind():

    1
    2
    
    #include <sys/socket.h>
    int bind(int fd, struct sockaddr* addressp, int addrlen);
    
    1. 返回值:
      1. 成功:0
      2. 失败:-1
    2. 参数:
      1. fd, 由socket()函数返回的套接字描述符
      2. addressp: 向协议传送地址的指针。包含名称端口IP地址信息。其格式取决于family,若为AF_UNIX,则地址为sockaddr_un

        绑定地址及端口号的设置方式见下表:

        应用程序 IP地址 端口号 说明
        服务器 INADDR_ANY 非0 指定服务器为公认端口号
        服务器 本地IP 非0 指定服务器IP地址和公认端口号
        客户机 INADDR_ANY 非0 指定客户机的连接端口号
        客户机 本地IP 非0 指定客户机的IP地址及连接端口号
        客户机 本地IP 0 指定客户机IP地址
      3. addrlen: 地址结构的字节数
  4. connect():

    1
    2
    
    #include <sys/socket.h>
    int connect(int fd, struct sockaddr * addressp, int addrlen);
    
    1. 返回值:
      1. 成功:0
      2. 失败:-1
    2. 参数:
      1. fd: 套接字描述符
      2. addressp: 套接字地址指针。地址的格式取决于family
      3. addrlen: 地址结构的字节数
  5. listen():

    1
    2
    
    #include <sys/socket.h>
    int listen(int fd, int qlen);
    
    1. 返回值:
      1. 成功:0
      2. 失败:-1
    2. 参数:
      1. fd: 套接字的文件描述符。套接字只能是SOCK_STREAM、SOCK_SEQPACKET类型
      2. qlen: 连接请求队列长度
    3. 说明:服务器可以使用listen()函数将所有的服务请求放在一个请求队列中排队
  6. accept():

    1
    2
    
    #include <sys/socket.h>
    int accept(int fd, sockaddr * addressp, int *addrlen);
    
    1. 返回值:
      1. 成功: 非负的文件描述符
      2. 失败:-1
    2. 参数:
      1. fd: 套接字的文件描述符
      2. addressp:
    3. 说明:面向连接的服务器执行了listen()函数后,执行accept()函数等待来自某一客户进程的实际连接请求

2.4 高级套接字函数

使用man <函数名>以了解详细使用方法,如man socket

  1. send(), sendto()
  2. sendmsg(), recvmsg()
  3. readv(), writev()
  4. close(), shutdown()
  5. getpeername(), getsockname()
  6. getsockopt(), setsockopt()
  7. fcntl(), ioctl()

2.5 多路复用

  1. 定义:多路复用通过设置文件描述符集,将多个套接字组成一个集合,然后使用select()函数对集合进行监控,集合中任何一个(或几个)描述符就绪时, 进程就可以作相应的I/O处理。文件描述符集分为异常三个类型,其中异常描述符集主要应用于带外数据的处理。
  2. 系统调用:

    1
    2
    3
    4
    5
    
    int select(int maxfd, struct fd_set* rdset, struct fd_set* wrset, struct fd_set* exset, struct timeval* timeout);
    void FD_SET(int fd, fd_set * fdset);
    void FD_CLR(int fd, fd_set * fdset);
    void FD_ZERO(fd_set* fdset);
    int FD_ISSET(int fd, fd_set* fdset);
    
  3. 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    
    int main(int argc, char argv[])
    {
    	int sockfd[NUMBER];
    	struct sockaddr_in servaddr[NUMBER];
    	fd_set rfds;
    	char buf[1024];
    	int i;
       	
    	for(i=0; i<NUMBER; i++){
    	    sockfd[i]=socket(AF_INET, SOCK_STREAM, 0);
    	    if(sockfd[i]<0)
    	        exit(1);
       	
    	}
    	 ... //填充NUMBER个地址结构
    	 ... //建立NUMBER个连接
       	
    	int nOK[NUMBER];
    	for(i=0; i<NUMBER; i++){
    	    nOK[i]=0;
    	}
    	int nEnd=NUMBER;
    	while(nEnd!=0){
    	    for(i=0; i<NUMBER; i++){
    	        if(nOK[i]==0)
    	 	       FD_SET(sockfd[i], &rds);
    	    }
    	    n=select(theMax(NUMBER, sockfd)+1, &rds, NULL, NULL, NULL);
    	    if(n<0 && errno==EINTR)
    			continue;
       
    		for(i=0; i<NUMBER; i++){
    			if(FD_ISSET(sockfd[i],&rds)){
    				n=read(sockfd[i], buf, 1024);
    				if(n<=0 && errno!=EINTR){
    					perror("An Error.");
    					nOK[i]=1;
    					nEnd--;
    				}
    				else if(n>0){
    					process(buf, ...); // 数据处理
    					nOK[i]=1;
    					nEnd--;
    				}
    			}
    		}
    	}
    	for(i=0; i<NUMBER; i++){
    		close(sockfd[i]);
    	}
    	return 0;
    }
    

2.6 网络字节传输顺序及主机字节顺序

  1. 大端小端:

    • 小端存储(little-endian):低字节在前,高字节在后
    • 大端存储(big-endian):高字节在前,低字节在后
      RISC芯片Internet采用大端存储Intel芯片采用小端存储
  2. 相关函数:htons(), htonl(), ntohs(), ntohl()。其中,h代表hostn代表networks代表shortl代表long

2.7 字节处理函数

  1. socket地址是多字节数据,不是以空字符结尾的,这和C语言中的字符串是不同的。Linux提供了两组函数来处理多字节数据:一组以b(byte)开头,是和BSD系统兼容的函数;另一组以mem(memory)开头,是ANSI C提供的函数。
    1. b开头的函数有:bzero(), bcopy(), bcmp()。头文件为<strings.h>
    2. mem开头的函数有:memset(), memcpy(), memcmp()。头文件为<string.h>

2.8 DNS与域名访问

  1. 域名系统:
    1. 由来:由于IP地址(如183.232.231.172)不好记,故产生了好记的域名(如www.baidu.com)
    2. Internet的层次命名机制:如ftp.xidian.edu.cn中:cn代表china,edu代表education,xidian代表西安电子科技大学,ftp代表其用于文件传输
    3. Internet的DNS结构:DNS层次结构
    4. 正式名称和别名:Internet允许某个网络或主机同时拥有多个域名,但只允许存在一个正式名称,而将其他名称视为别名。主机的别名可以对应到相同或不同的主机
  2. 域名服务器:
    1. 定义:域名和IP地址之间的映射,包括正向解析(从域名到地址)以及逆向解析(从地址到域名)。这种映射是由一组域名服务器完成的。与域名系统相同,域名服务器也是层次型的
    2. 资源记录:
      1. A类记录:master.utopian.edu.cn IN A 203.58.0.20
      2. MX类记录:utopian.edu.cn 86400 IN MX 1 master.utopian.edu.cn
      3. CNAME类记录:ftp.utopian.edu.cn 86400 IN CNAME master.utopian.edu.cn
  3. 基于IP和域名的通信编程:
    1. 相关函数:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      #include <netdb.h>
      
      struct hostent* gethostbyname(const char* name);
      struct hostent{
          char *h_name; //主机的正式名称
          char **h_aliases; //以NULL结尾的数组,存储了主机的备用名称
          int h_addrtype; //主机地址的类型,一般为AF_INET
          int h_length; //主机地址长度
          char **h_addr_list; //以NULL结尾的数组,存储了主机的地址
      }
      #define h_addr h_addr_list[0]
      
    2. 错误显示:如果gethostbyname函数查找域名地址失败,则将使用全局变量h_errno(注意区别errno),并且应用程序使用herror(注意区别perror)函数来输出错误提示。常见错误有:HOST_NOT_FOUND, NO_ADDRESS, NO_RECOVERY, TRY_AGAIN。详情参见常用域名记录解释:A记录、MX记录、CNAME记录、TXT记录、AAAA记录、NS记录
    3. 示例程序:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      
      #include <stdio.h>
      #include <stdlib.h>
      #include <errno.h>
      #include <netdb.h>
      #include <arpa/inet.h>
            
      int main(int argc, char *argv[])
      {
      	struct hostent *h;
      	if(argc!=2){
      		fprintf(stderr, "usage: getip <address>\n");
      		exit(1);
      	}
      	if((h=gethostbyname(argv[1]))==NULL){
      // printf("gethostbyname\n");
      		herror("agethostbyname: Uknow host");
      		exit(1);
      	}
      	printf("Host name: %s\n", h->h_name);
      	printf("Host aliases: \n");
      	int i;
      	for(i=0; h->h_aliases[i]!=NULL; i++){
      		printf("%d: %s\n", i+1,  h->h_aliases[i]);
      	}
      	printf("\nIP Address: %s\n", inet_ntoa(*((struct in_addr*)h->h_addr)));
      	printf("Address list:\n");
      	for(i=0; h->h_addr_list[i]!=NULL; i++){
      		printf("%d: %s\n", i+1, inet_ntoa(*((struct in_addr*)h->h_addr_list[i])));
      	}
      	return 0;
      }
      

2.9 基于TCP套接字编程示例

  1. 概述:服务器通过socket连接向客户端发送字符串“Hello, you are connected!”。只要在服务器上运行该服务器软件,在客户端启动客户软件时,客户端就会收到该字符串并显示,表示客户机和服务器连接成功。

  2. 服务器: 首先调用socket函数创建一个socket, 然后调用bind函数将其与本机地址以及一个本地端口号绑定,再调用listen在相应的socket上监听。当accept接收到一个连接服务请求时,将生成一个新的socket向客户端发送字符串“Hello, you are connected!”。最后关闭该socket。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    
    /* tcp_server.c
     */
       
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <strings.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <sys/stat.h>
    #include <fcntl.h>
       
    #define SERVPPORT 3333 //服务器监听端口号
    #define BACKLOG 10 //最大同时连接请求数
       
    int main()
    {
    	int sockfd, client_fd; //监听socket和数据传输socket
    	int sin_size;
    	struct sockaddr_in my_addr; //本机地址信息
    	struct sockaddr_in remote_addr; //客户端地址信息
       
    	if((sockfd=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))==-1){
    		perror("socket");
    		exit(EXIT_FAILURE);
    	} //建立套接字
       
    	//地址段填充
    	my_addr.sin_family=AF_INET;
    	my_addr.sin_port=htons(SERVPPORT);
    	my_addr.sin_addr.s_addr=INADDR_ANY;
    	bzero(&(my_addr.sin_addr), 8);
       
    	if(bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr))==-1){
    		perror("bind");
    		exit(EXIT_FAILURE);
    	} //绑定
       	
    	if(listen(sockfd, BACKLOG)==-1){
    		perror("listen");
    		exit(EXIT_FAILURE);
    	} //监听
       
    	while(1){
    		sin_size=sizeof(struct sockaddr_in);
    		if((client_fd=accept(sockfd, (struct sockaddr*)&remote_addr, &sin_size))==-1){
    			perror("accept");
    			continue;
    		} //等待并接受连接
    		printf("received a connection from %s: %d\n", inet_ntoa(remote_addr.sin_addr), remote_addr.sin_port);
    		if(!fork()){
    			//子进程代码
    			if(send(client_fd, "Hello, you are connected!\n", 26, 0)==-1){
    				perror("send");
    			}
    			close(client_fd);
    			exit(EXIT_SUCCESS);
    		} //创建子进程发送数据
    		close(client_fd);
    	}
    	return 0;
    }
    
  3. 客户端:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    /* tcp_client.c
     */
       
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <strings.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <netdb.h>
    #include <string.h>
       
    int main(int argc, char *argv[])
    {
    	int sockfd;
    	char buffer[1024];
    	struct sockaddr_in server_addr;
    	struct hostent *host;
    	int portnumber, nbytes;
    	if(argc!=3){
    		fprintf(stderr, "Usage: %s hostname portnumber\a\n", argv[0]);
    		exit(EXIT_FAILURE);
    	}
    	if((host=gethostbyname(argv[1]))==NULL){
    		fprintf(stderr, "Gethostname error\n");
    		exit(EXIT_FAILURE);
    	}
    	if((portnumber=atoi(argv[2]))<0){
    		fprintf(stderr, "Usage: %s hostname portnumber\a\n", argv[0]);
    		exit(EXIT_FAILURE);
    	}
    	// 客户程序开始建立sockfd描述符
    	if((sockfd=socket(AF_INET, SOCK_STREAM, 0))==-1){
    		fprintf(stderr, "Socket Error: %s\a\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    	// 客户程序填充服务器的资料
    	bzero(&server_addr, sizeof(server_addr));
    	server_addr.sin_family=AF_INET;
    	server_addr.sin_port=htons(portnumber);
    	server_addr.sin_addr=*((struct in_addr*)host->h_addr);
    	// 客户程序发起连接请求
    	if(connect(sockfd, (struct sockaddr*) (&server_addr), sizeof(struct sockaddr))==-1){
    		fprintf(stderr, "connect error: %s\a\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    	// 连接成功了
    	if((nbytes=read(sockfd, buffer, 1024))==-1){
    		fprintf(stderr, "Read Error: %sn", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    	buffer[nbytes]='\0';
    	printf("I have received: %s\n", buffer);
    	// 结束通信
    	close(sockfd);
    	return 0;
    }
    

3 UDP套接字与原始套接字的编程

3.1 概述

UDP(用户数据报协议)是一个面向无连接的传输协议,是一个简单的面向数据报的传输层协议。UDP只提供数据的不可靠传递,它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份(所以UDP有时候也被认为是不可靠的数据报协议)。UDP在IP数据报的头部仅仅加入了复用和数据校验(字段)。

UDP报头
偏移 字节 0 1 2 3
 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 0 来源连接端口 目的连接端口
4 32 报文长度 校验和