《Unix网络编程》卷1 初级

《Unix网络编程》卷1

  • 第1章 简介
  • 第2章 传输层TCP-UDP-SCTP
  • 第3章 套接字编程简介
    • 套接字的地址结构
    • 值结果参数
    • 字节排序函数
    • 字节操纵函数
    • inet_pton和inet_ntop函数
    • scok_ntop和相关函数
    • readn, writen, readline函数

第1章 简介

  • 网络应用系统主要构成有两部分:客户端(client)和服务器(server)。
    • 举例来说:web服务器程序时一个长时间运行的守护程序,web客户与服务器之间使用TCP通信,TCP转而使用IP通信,IP通过以太网驱动程序的数据链路层通信。
      《Unix网络编程》卷1 初级_第1张图片
  • 客户端和服务器通常是用户进程,而TCP和IP协议通常是内核中"协议栈"的一部分。
  • 网络:
    • 分类
      • LAN:局域网(内网)
      • WAN:广域网(外网)
    • 路由器是广域网的架构设备。当下最大的广域网是因特网internet。
      《Unix网络编程》卷1 初级_第2张图片
  • 时间获取客户程序server
  • Code
    /* 一个简单的时间获取客户程序server */
    #include 
    #include "unp.h"
     
    #define MAXLINE 4096
    #define LISTENQ 1024
    //#define SA struct sockaddr
    typedef struct sockaddr SA;
     
    int
    main(int argc, char **argv)
    {
    	int					listenfd, connfd;
    	struct sockaddr_in	servaddr;
    	char				buff[MAXLINE];
    	time_t				ticks;
     	// 使用socket创建一个网际(AF_INET)字节流(SOCK_STEREAM)套接字,
     	// 返回类型为整数类型描述符, 后面的函数调用(如 connect, read等)就使用该描述符来标识此套接字
    	listenfd = socket(AF_INET, SOCK_STREAM, 0);
     
    	bzero(&servaddr, sizeof(servaddr));
    	servaddr.sin_family      = AF_INET;
    	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    	servaddr.sin_port        = htons(1300);	/* daytime server */
     
    	bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
     	// 调用listen函数将套接字转换为监控套接字
    	listen(listenfd, LISTENQ);
     	// 接受服务器链接,发送应答
    	for ( ; ; ) {
    		connfd = accept(listenfd, (SA *) NULL, NULL);
     
            ticks = time(NULL);
            snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
            write(connfd, buff, strlen(buff));
     
    		close(connfd);
    	}
    }
     
    /* 一个简单的时间获取客户程序client */
    #include	"unp.h"
     
    #define MAXLINE 1024
    //#define SA struct sockaddr
    typedef struct sockaddr SA;
     
    int main(int argc, char **argv)
    {
    	int					sockfd, n;
    	char				recvline[MAXLINE + 1];
    	struct sockaddr_in	servaddr;
     
    	if (argc != 2)
    		printf ("usage: a.out \n");
     	// 使用socket创建一个网际(AF_INET)字节流(SOCK_STEREAM)套接字,
     	// 返回类型为整数类型描述符, 后面的函数调用(如 connect, read等)就使用该描述符来标识此套接字
    	if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    		printf ("socket error\n");
     	// 把IP和Port填入一个网际套接字地址结构(名为 servaddr和sockaddr_in的结构变量)
     	// 1. 使用bzero将结构体清空
    	bzero(&servaddr, sizeof(servaddr));
    	servaddr.sin_family = AF_INET; // 2. 置地址族为 AF_INET
    	servaddr.sin_port   = htons(1300);	/* daytime server */ // 3. 置位端口
    	if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) // 4. 置位IP
    		printf ("inet_pton error for %s\n", argv[1]);
     	// 建立和服务器的链接
    	if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
    		printf ("connect error\n");
     	/// 读入并输出服务器应答 
     	// 使用read函数读取服务器应答,使用标准输出 fputs输出结构
    	while ( (n = read(sockfd, recvline, MAXLINE)) > 0) { 
    		recvline[n] = 0;	/* null terminate */
    		if (fputs(recvline, stdout) == EOF)
    			printf ("fputs error\n");
    	}
    	if (n < 0)
    		printf ("read error\n");
     
    	exit(0);
    }
     
    /* unp.h */
    #ifndef __UNP_H__
    #define __UNP_H__
     
    #include		/* basic system data types */
    #include		/* basic socket definitions */
    #include		/* timeval{} for select() */
    #include			/* timespec{} for pselect() */
    #include		/* sockaddr_in{} and other Internet defns */
    #include		/* inet(3) functions */
    #include	
    #include			/* for nonblocking */
    #include	
    #include	
    #include	
    #include	
    #include	
    #include		/* for S_xxx file mode constants */
    #include			/* for iovec{} and readv/writev */
    #include	
    #include	
    #include			/* for Unix domain sockets */
     
    #endif //__UNP_H__
    
    • 一些 Tricks:
      • 调用sprintf无法检查目的缓冲区是否溢出,相反,snprintf要求其第二个参数指定目的缓冲区的大小,因此可以确保该缓冲区不溢出。
      • 许多网络入侵是由黑客通过发送数据,导致服务器对sprintf的调用使其缓冲区溢出而发生的,必须小心使用的函数还有gets/strcat/strcpy,通常应分别改为调用fgets/strncat/strncpy,更好的替代函数还有strlcat/strlcpy可以确保结果是正确终止的字符串。

  • OSI模型 open systems interconnection(全称:计算机通信开放系统互连模型。)
    • 分层:
      • 物理层/数据链路层:主要是设备驱动和网络硬件,通常我们不必关心。
      • 网络层:由IPv4和IPv6这两个协议处理。详细在附录A中。
      • 传输层:即本书所讲的套接字编程接口,从应用层(上3层)进入传输层的接口。
      • 应用层/会话层/表示层:统称为应用层,如web客户端(浏览器)、telnet客户端、web服务器、FTP服务器等。
    • 结构图
      《Unix网络编程》卷1 初级_第3张图片
  • 网络相关调试命令:
    • 网络细节的三个基本命令: netstat / ifconfig / ping
      • netstat
        • netstat -ni // 提供网络接口信息,-n输出数值地址而不是反向解析为名字
          • $ netstat -ni
            Kernel Interface table
            Iface   MTU Met   RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
            eth0       1500 0     15459      0      0 0         10444      0      0      0 BMRU
            lo        16436 0       138      0      0 0           138      0      0      0 LRU
            // lo 环回接口
            // eth0 以太网接口
            
        • netstat -nr // 展示路由表信息,另一种确定接口的方法
          • $ netstat -nr
            内核 IP 路由表
            Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
            0.0.0.0         192.168.31.1    0.0.0.0         UG        0 0          0 eth0
            169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth0
            192.168.31.0    0.0.0.0         255.255.255.0   U         0 0          0 eth0
            
      • ifconfig
        • ifconfig eth0 // 获得eth0以太网接口的详细信息
          • $ ifconfig eth0
            eth0      Link encap:以太网  硬件地址 00:0c:29:55:a0:99
                      inet 地址:192.168.31.205  广播:192.168.31.255  掩码:255.255.255.0
                      inet6 地址: fe80::20c:29ff:fe55:a099/64 Scope:Link
                      UP BROADCAST RUNNING MULTICAST  MTU:1500  跃点数:1
                      接收数据包:15624 错误:0 丢弃:0 过载:0 帧数:0
                      发送数据包:10571 错误:0 丢弃:0 过载:0 载波:0
                      碰撞:0 发送队列长度:1000
                      接收字节:1468669 (1.4 MB)  发送字节:1070042 (1.0 MB)
                      中断:19 基本地址:0x2000
            // MULTICAST 标志通常指明该接口所在主机支持多播。
            
      • ping // 测试ip地址是否联通当前以太网络
        • $ ping -b 192.168.31.255
          PING 192.168.31.255 (192.168.31.255) 56(84) bytes of data.
          64 bytes from 192.168.31.255: icmp_req=1 ttl=64 time=0.253 ms
          64 bytes from 192.168.31.255: icmp_req=2 ttl=64 time=0.022 ms
          64 bytes from 192.168.31.255: icmp_req=3 ttl=64 time=0.029 ms
          
  • 64位体系结构的趋势原因之一是:在每个进程内部可以由此使用更长的编址长度(即64位指针),从而可以寻址更大的内存空间(超过2^32字节)。

第2章 传输层TCP-UDP-SCTP

  • 协议类型:
    • TCP:传输控制协议,面相连接,全双工字节流。流套接字。关心:确认、超时、重传等细节。
    • UDP:用户数据报协议,无连接协议。数据报套接字。不保证最终达到目的地。
    • ICMP:网际控制消息协议,处理路由器和主机之间流通的错误和控制消息。
    • ARP:地址解析协议,把IPv4地址映射成一个硬件地址。
    • RARP:反地址解析协议,把一个硬件地址映射成一个IPv4地址。

  • TIME_WAIT状态有两个存在的理由:
    • 可靠地实现TCP全双工连接的终止;
    • 允许老的重复分节在网络中消逝。

  • 端口号port:
    • 传输层协议都使用16位整数的端口号来区分不同的进程。
    • 0~ 1023(自控保留) | 1024~ 49151(已登记) |49152~ 65535(动态私有)

  • 通讯对socket pair:
    • 每个TCP分节中都有16bit的端口号和32bit的IPv4地址。

第3章 套接字编程简介

  • 几乎每一个例子都用到了套接字地址结构. 这些结构可以在两个方向上传递: 从 进程到内核内核到进程 .
  • 地址转换函数在地址的文本表达 和他们存放在套接字地址结构中的二进制之间进行转换
    • inet_addr/inet_ntoa 适用于IPv4
    • inet_npton/inet_ntop 适用于IPv4/IPv6

套接字的地址结构

  • 套接字地址结构在给定主机上使用:
    • 虽然其定义了某些字段用于不同主机间进行通信,但是结构体本省并不在主机间进行传递
  • IPv4套接字地址结构
    • sin_zero 为使用,我们应该把它置位0,按照惯例我们应该先把结构体置位0,而不是单单把sin_zero置位0
      // IPv4套接字地址结构:sockaddr_in
      #include 
      
      struct in_addr {
          in_addr_t       s_addr;         /* 32bit IPv4 address. */
      };
      
      struct sockaddr_in {
          uint8_t         sin_len;        /* length of structure (16) */
          sa_family_t     sin_family;     /* AF_INET */
          in_port_t       sin_port;       /* 16bit TCP/UDP port number */
          struct in_addr  sin_addr;       /* 32bit IPv4 address */
          char            sin_zero[8];    /* unused */ 
      };
      
  • IPv6套接字地址结构: sockaddr_in6
    • IPv6
      IPv6套接字地址结构:sockaddr_in6
      #include 
      
      struct in6_addr {
          uint8_t         s6_addr[16];    /* 128bit IPv6 address */
      };
      
      #define SIN6_LEN
      struct sockaddr_in6 {
          uint8_t         sin6_len;       /* length of structure (28) */
          sa_family_t     sin6_family;    /* AF_INET6 */
          in_port_t       sin6_port;      /* transport layer port */
          uint32_t        sin6_flowinfo;  /* flow information, undefined */
          struct in6_addr sin6_addr;      /* IPv6 address */
          uint32_t        sin6_scope_id;  /* set of interfaces for a scope */
      };
      
  • 通用套接字地址结构: sockaddr
    • 套接字函数被定义为以指向某个通用套接字地址结构的指针作为其参数之一,例如bind函数
      • int bind(int, struct sockaddr*, socklen_t) // 注意第二个参数为通用套接字地址结构
      • 所以对这些函数的任何调用都必须把指向特定协议的套接字地址结构的指针 进行强转为指向通用套接字地址结构的指针.
        • 如:bind(sockfd, (struct sockaddr*) &serv, sizeif(serv));
      • 从代码狗得角度来看,这些通用套接字地址结构得唯一用途就是对指向特定协议族得套接字地址结构得指针进行强制类型转换.
    	// 通用套接字地址结构:sockaddr
    	#include 
    	
    	struct sockaddr {
    	    uint8_t         sa_len;         /* length of structure */
    	    sa_family_t     sa_family;      /* address family: AF_XXXX value */
    	    char            sa_data[14];    /* protocol-specific address */
    	};
    	```
    - 新的通用套接字地址结构: `sockaddr_storage`
    	- 特点:
    		- 所过系统支持得任何套接字地址结构有对齐需求,那`sockaddr_storage`满足最严格得对齐需求
    		- `sockaddr_storage`足够大,注意容纳系统支持得任何套接字地址结构
    		- 出了`ss_familly`和`ss_len`外`sockaddr_storage`结构中得其他字段对用户都是透明得
    	```c
    	#include 
    	struct sockaddr_strorage {
    	    uint8_t         ss_len;         /* length of structure */
    	    sa_family_t     ss_family;      /* address family: AF_XXXX value */
    	};
    	```
    
  • 套接字比较
    《Unix网络编程》卷1 初级_第4张图片

值结果参数

  • 内核和进程之间的相互复制
    • 从进程到内核传递套接字地址结构的函数有3个:bind,connect, sendto,这些函数的参数是指向某个套接字地址结构的指针,另一个参数是改结构的整数大小,既然指针和指针所指的内容的大小都传递给了内核,于是内核知道需要从进程复制缩少数据
    • 从内核到进程传递套接字地址结构的函数有4个:accept,recvfrom,getsockname,getpeername,这四个函数的其中两个参数是指向某套接字地址结构的指针,和指向该套接字地址结构的内容的大小(是两个指针,分别指向套接字地址结构和套接字地址结构的大小)
  • 当函数被调用时,结构大小是一个值(此值告诉内核该结构的大小,使内核在写此结构时不至于越界),当函数返回时,结构大小又是一个结果(它告诉进程内核在此结构中确切存储了多少信息),这种参数类型叫值结果参数
    《Unix网络编程》卷1 初级_第5张图片

字节排序函数

  • 大端:高字节放低地址,如0x0102,内存中放的是0x0102
  • 小端:高字节放高地址,低字节放低地址如0x0102,内存中放的是0x0201
  • 网际协议使用大端字节序传送多字节整数
    #include 
    uint16_t htons (uint16t host16bitvalue): //h:host  主机字节序
    uint32_t htonl (uint32t host32bitvalue); //n:network 网络字节序
    uint16_t ntohs (uint16t net16bitvalue);  //s:short
    uint32_t ntohl (uint32t net32bitvalue);  //l:long
    

字节操纵函数

  • str开头:处理C字符串(即以\0结尾)
  • b开头:起源与4.2BSD,这里给出源自Berkeley的函数:
  • b系列
    • bzero
      • void bzero(void *dest, size_t nbytes);
      • 把目标字节串中指定数目的字节置为0.
    • bcopy
      • void bcopy(const void *src, void *dest, size_t nbytes);
      • 将指定数目的字节从源字节串移到目标字节串
    • bcmp
      • int bcmp(const void *ptrl, const void *ptr2, size_t nbytes);
      • 比较两个任意的字节串,若相同返回0,不同返回非0
      #include
      void bzero(void *dest, size_t nbytes);
      void bcopy(const void *src, void *dest, size_t nbytes);
      int bcmp(const void *ptrl, const void *ptr2, size_t nbytes);
      
  • m系列
    • memset
      • void *memset(void *dest, int c, size_t len);
      • 把目标字符串指定数目的字节置为c
    • memcpy
      • void *memcpy(void *dest, const void *src, size_t nbytes);
      • 类似bcopy,不过两个指针参数的顺序相反。当源字节串与目标字节串重叠时,bcopy能够正确处理,但是memcpy的操作结果却不可知。这种情形下必须该用ANSI C的memmove函数。
    • memcmp
      • int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
      • 比较两个任意的字节串,若相同返回0,否则返回一个非0值
      #include 
      void *memset(void *dest, int c, size_t len);
      void *memcpy(void *dest, const void *src, size_t nbytes);
      int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
      

inet_pton和inet_ntop函数

  • 这个两个函数的作用是实现ip地址点分十进制格式和二进制格式的相互转换。
  • p指表达式, n指数值,地址的表达格式通常是ASCII字符串,而在套接字地址结构中这是二进制值
  • inet_pton: 尝试转换由strptr指针所指的字符串,并通过addrptr指针存放二进制结果
    • int inet_pton(int family, const char *strptr, void *addrptr);
      • family:AF_INET或AF_INET6
      • 返回:1(成功) 0(不是有效格式) -1(出错)
  • inet_ntop: inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达格式(strptr)。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。
    • const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
      • family:AF_INET或AF_INET6
      • 指定size_t防止溢出
        • #define INET_ADDRSTRLEN 16
        • #define INET6_ADDRSTRLEN 46
      • 返回:结果指针(成功) NULL(失败)
      #include 
      int inet_pton(int family, const char *strptr, void *addrptr);
      //返回:1(成功) 0(不是有效格式) -1(出错)
      const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
      //返回:结果指针(成功) NULL(失败)
      

scok_ntop和相关函数

  • inet_ntop需要知道地址族, 地址结构中的二进制地址指针
// IPv4
struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
> // IPv6
struct sockaddr_in6 addr6;
inet_ntop(AF_INET6, &addr6.sin_addr, str, sizeof(str));
  • 以上过程还是比较繁琐的,为了简便起见,我们定义一个sock_ntop函数, 它的参数为
    • 套接字地址结构指针
    • 套接字地址结构的长度
      char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen)
/* include sock_ntop */
char *sock_ntop(const struct sockaddr *sa, socklen_t salen)
{
    char        portstr[8];
    static char str[128];       /* Unix domain is largest */

    switch (sa->sa_family) {
    case AF_INET: {
        struct sockaddr_in  *sin = (struct sockaddr_in *) sa;
        //地址转换:成功则返回c字符串形式的IP地址,str指定转换格式
        if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
            return(NULL);
        //字节排序:网络转换为主机的字节序
        if (ntohs(sin->sin_port) != 0) {
            snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
            strcat(str, portstr);
        }
        return(portstr);
    }
    //。。。。。。

readn, writen, readline函数

  • 字节流套接字(如TCP套接字)上的read和write函数所表现的行为不同于通常的文件I/O.字节流套接字上调用的read和write输入和输出的字节数可能比请求的数量少,这并不是出错. 原因是 内核中用于套接字的缓冲区可能已经达到极限了.
    为了以防万一, 不让实现返回不足的字节,使用writen和readn来改进函数
  • readn
    • ssize_t readn(int filedes, void *buff, size_t nbytes)
    • 返回: 读入字节数, 出错返回1
  • written
    • ssize_t written(int filedes, void *buff, size_t nbytes)
    • 返回: 写入字节数, 出错返回1
  • readline非常低效,其每读一个字节 就调用依次系统read函数
    • ssize_t readline(int filedes, void *buff, size_t maxlen)
    • 返回: 读入字节数, 出错返回1
ssize_t                     /* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
    size_t  nleft;
    ssize_t nread;
    char    *ptr;

    ptr = vptr;
    nleft = n;
    while (nleft > 0) {

        //如果读取失败
        if ( (nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR) /* 查找EINTR错误,表示系统被一个捕获信号中断 */
                nread = 0;      /* and call read() again */
            else
                return(-1);
        //如果读成功了    
        } else if (nread == 0)
            break;              /* EOF */
        //计算漏读的字节数,再读文件
        nleft -= nread;
        ptr   += nread;
    }
    return(n - nleft);      /* return >= 0 */
}
/* end readn */

你可能感兴趣的:(UNIX,网络编程)