UNP_Chapter01_Introduction_笔记总结

            ***PAY A TRIBUTE TO W.Richard Stevens***

Chapter01: UNP入门介绍

1.1 简介

当我们写一个程序要想在网络中进行传输的时候,就必须考虑到需要一个协议能够对网络中传输的细节问题做统一的约束.
比如说,Web服务器一般都是一个永久运行,不down机或者说是一个守护进程在服务器端. 再比如我们所熟知的CS模型,
即client/server模型,谁负责起初始化,谁负责请求,谁负责回应等等. 当然在绝大多数的CS模型中,都是客户端向服务器
端发送初始化请求,服务器端进行回应的,这也简化了好多协议和程序本身.但是也有一些模型是需要相互的初始化,相互的
请求与回应.
一般情况下我们客户端在同一时间只能和一个服务器端进行交互,但是服务器端,同一时间处理多个客户端是司空见惯的.
这里写图片描述
从浅层来看,客户端程序和服务器段程序是通过网络中的协议进行交流的,但是其中涉及到了很多网络协议. 但是我们这里只
主要讨论TCP/IP套装,比如说我们Web客户端访问服务器端的时候,传输层经TCP(Transmission control protocol)然后转给
网络层也就是IP层(Intenet protocol),之后再转给数据链路层.如果客户端和服务器端在一个以太网内,那么就是如下这种模式:
这里写图片描述
在这里,应用层的客户端和服务器端就是典型的用户空间的进程,传输层入TCP和网络层如IP协议作为协议栈的一部分就存在于
内核中了.
传输层中我们不仅讨论TCP,我们也会讨论UDP(User Datagram Protocol).网络层中我们通常所说的IP,其实是1980s的IP version
4(IPv4), 我们也会讨论IPv6的. 当然交互的两端不仅仅能在局域网(LAN)中,也是可以存在在WAN中的,当今最大的WAN就是Internet:
这里写图片描述
许多公司都自己搭建字节的WAN, 并且这些私有WAN可能并没有接入到Internet中.
本章主要就是概述以后会详解的知识点,我们全程都会以服务器客户端模型引入,虽然简单,但是其中涉及到了很多很多的知识点.

1.2 Daytime客户端

接下来我们来简单写个让服务器端返回日期的CS模型热热手,在这里我用的是Richard的代码,但是他自己还写了个unp.h,我会将每个代码
中用到的被unp.h封起的宏或函数都摔出来,也就是我不用unp.h,这样更会有助于理解知识点:
参考: socket(2), bzero(3), ip(7), htons(3), inet_pton(3), connect(2))

#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#define SA      struct sockaddr
#define MAXLINE 4096
int main(int argc, char *argv[])
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in servaddr;

    if(argc != 2)
    {
        perror("usage: dateCS <IP Address>");
    }
    /*int socket(int domain, int type, int protocol);*/
    /*tcp_socket = socket(AF_INET, SOCK_STREAM, 0);*/
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0) < 0))
    {
        perror("socket err");
    }
/*void bzero(void *s, size_t n);*/
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(13);

    /*int inet_pton(int af, const char *src, void *dst);*/
    if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        perror("inet_pton error");
    }
/*int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);*/
    if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("connect err");
    }
    while((n = read(sockfd, recvline, MAXLINE)) > 0)
    {
        recvline[n] = 0;//null terminate
        if(fputs(recvline, stdout) == EOF)
        {
            perror("fputs err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    exit(0);
}

这里涉及到很多的知识点,看不大懂也没关系,这些函数都很重要,并且我们在之后都会一一介绍到. 在这里socket函数创造了
一个Internet流socket,通过参数AF_INET表示Internet(这里说到Internet就默认是IPV4), SOCKET_STREAM表示是一种流协议,
其实就是TCP. 这个函数返回的 一个整数描述符就是我们之后要用到识别socket的.在这里我们还用到一个Internet的socket地址
结构体: sockaddr_in. 我们先 通过bzero用0将这个结构体清空, 然后塞入AF_INET,port和IP. 并且在这个结构体中IP和port都得
用指定的格式进行传输, 所以我们用htons(主机字节序转换成网络字节序)转换二进制端口号,inet_pton转换ASCII的命令行参数.
读者可能会有疑问,为什么要用bzero而不用memset?是因为bzero要比memset方便记忆,基本上每个Unix厂商都会有bzero方法
的. 他们的效果是一样一样的.
我们定义了一个宏#define SA struct sockaddr, 是一个通用socket地址结构体.就是任何的socket函数要求指向socket地址结构
体的时候都要强制转换成通用结构体.这是因为socket函数要早于ANSI C标准, 所以void*是不允许的.
在这里需要注意的一点是TCP是字节流协议,是没有记录边界的,也就是并不会知道你想要在哪里截断这波传输.
在这个实例中, 整个传输的结束是由服务器端在传输完成后直接断开的.这很类似于HTTP/1.0. 但比如SMTP中,这波传输的结束是
服务器端传输回两个字节的序列号紧跟这换行(\n, linefeed). RPC和DNS会将返回文本的二进制长度添加到记录前面用当用TCP
进行传输的时候.这里要说明的就是TCP本身是没有给你任何的结束或开始标志的,什么\r\n都是没有的.如果应用程序想要达到
在指定位置结束这波传输,就必须在传输文本前后做些手脚.

1.3 协议的独立性

这里做一下对比,如果我们不用Ipv4而是用ipv6:

int main(int argc, char *argv[])
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in6 servaddr;

    if(argc != 2)
    {
        perror("usage: dateCS <IP Address>");
    }
    /*int socket(int domain, int type, int protocol);*/
    /*tcp_socket = socket(AF_INET, SOCK_STREAM, 0);*/
    if((sockfd = socket(AF_INET6, SOCK_STREAM, 0) < 0))
    {
        perror("socket err");
    }
/*void bzero(void *s, size_t n);*/
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin6_family = AF_INET6;
    servaddr.sin6_port = htons(13);

    /*int inet_pton(int af, const char *src, void *dst);*/
    if(inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0)
    {
        perror("inet_pton error");
    }
/*int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);*/
    if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("connect err");
    }
    while((n = read(sockfd, recvline, MAXLINE)) > 0)
    {
        recvline[n] = 0;//null terminate
        if(fputs(recvline, stdout) == EOF)
        {
            perror("fputs err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    exit(0);
}

这里我们发现其实仅仅修改了几个宏而已,因为不同的协议会产生不同的方式去调用,所有在我们以上的两端代码中就显示出了弊端,仅仅因为
修改几行代码就重新打个c文件.在之后我们会引入一个函数,getaddrinfo(3),这个函数的解释是这样的:
这里写图片描述
返回一个或多个addrinfo结构体, 包括网络地址,不管是v4还是v6,返回的结果都能直接用bind(), connect()直接使用.
我们的代码中的另外一个弊端就是用户通常是不愿意输入IP地址的,而更喜欢输入域名.在之后的章节中,我们会讲解如何进行地址的转换的.e

1.4 错误处理:封装方法

在我们写代码的时候,检查每个返回值是一定要做的.所以在UNP这本书中作者就封装了一些错误处理的方法,绝大多数对程序的执行效率并
不会有实质性的改变,但是对操作很方便.比如socket(2)函数,作者就定义了一个Socket函数,其中这个函数还直接进行了返回值的判断.这
样做的好处再比如,线程函数中pthread_错误返回时,会返回错误码,但是并不会自动的给errno赋值,所以我们要想打印出准确的错误信息,
我们必须每次调用这个函数的时候定义一个变量接到这个返回的错误码然后手动赋值给errno,然后再调用perror等函数.但是我会尽量给大家
还原最原始的代码.

1.5 Daytime 服务器

  1. 创建TCP socket,这个是和client一样的
  2. 绑定服务器的端口给创建的socket.这里调用的函数是bind(2). 并且我们制定IP地址是INADDR_ANY,表示我们向全世界开放.
  3. 将创建的普通socket转换成监听socket.这里调用函数listen(2),这样从客户端进来的连接都会交给kernel处理了.LISTENQ是说kernel接收
    客户端监听队列的最大数量.我们在之后会详解介绍.
#include <stdio.h>
#include <time.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#define MAXLINE         4096    /* max text line length */
#define LISTENQ         1024    /* 2nd argument to listen() */
#define SA      struct sockaddr
/*Richard老爷爷定义的这些函数我就都写出来了*/
/* include Socket */
int
Socket(int family, int type, int protocol)
{
        int             n;

        if ( (n = socket(family, type, protocol)) < 0)
/*                err_sys("socket error");*/
//我们为了方便,就不写这样的错误方法了
        perror("socket error");
        return(n);
}
/* end Socket */
void
Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
        if (bind(fd, sa, salen) < 0)
                perror("bind error");
}
/* include Listen */
void
Listen(int fd, int backlog)
{
        char    *ptr;

                /*4can override 2nd argument with environment variable */
        if ( (ptr = getenv("LISTENQ")) != NULL)
                backlog = atoi(ptr);

        if (listen(fd, backlog) < 0)
                perror("listen error");
}
/* end Listen */
int
Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
        int             n;
again:
        if ( (n = accept(fd, sa, salenptr)) < 0) {
#ifdef  EPROTO
                if (errno == EPROTO || errno == ECONNABORTED)
#else
                if (errno == ECONNABORTED)
#endif
                        goto again;
                else
                        perror("accept error");
        }
        return(n);
}
void
Write(int fd, void *ptr, size_t nbytes)
{
        if (write(fd, ptr, nbytes) != nbytes)
                perror("write error");
}
void
Close(int fd)
{
        if (close(fd) == -1)
                perror("close error");
}
int main(int argc, char **argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    time_t ticks;
    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(8888);
    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);
    for(;;)
    {
        connfd = Accept(listenfd, (SA *)NULL, NULL);
        ticks = time(NULL);
        snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buf, strlen(buf));
        Close(connfd);
    }
}

这样的话我们就能够与客户端执行了:
这里写图片描述
注意我这里将端口号改成了8888,是因为13是被系统服务占用这的.
在这波代码中我将作者的封装代码扣了出来,然后我错误处理函数使用的是perror,当然大家在做实验的时候可以模仿作者的错误处理函数自己
封装一下,作者错误源码lib/error.c:
这里写图片描述
这里写图片描述
在之后的代码中我将封装代码的位置会告知大家,自行可以去查看实现,都是很简单的,基本都是多打包了一层错误处理.不然的话,代码太长了.
通常状态下,服务器端执行到accept方法的时候会睡眠,等待客户端的到达并且被接收.TCP用三次握手的方式建立连接.当握手结束的时候,
accept返回,并且返回一个新的fd是链接成功的fd,这个fd就是正儿八经和客户端传输数据的fd. time这个函数返回的自1970年开始的UTC秒数,所以我们用ctime(3)转换成人类可读的字符串形式.
另外一点,在这里我们用到的是snprintf函数而不是很多人喜欢用的sprintf函数.大家应该更加习惯于用snprintf,放弃sprintf,因为用sprintf
你是检测不出buf是否overflow了,但是用snprintf你必须在第二个参数传入buf_size. 在显示生活中,有很多的黑客就是通过发送数据致使服务器的sprintf函数overflow.同样的类似于gets, strcat, strcpy 都要替换成fgets, strncat, strncpy甚至strlcat和strlcpy,确保程序的健壮.
服务器端最后通过调用close(2)函数触发了终止序列:FIN我们会在下一章主要介绍.
在之后我们还会涉及到很多很多的知识点,比如多并发,守护进程, 监听等待队列等.

1.6 OSI模型

想必有人也会混淆OSI,ISO吧,我们网络最标准的定义组织是ISO(International Organization for Standardization)其中的OSI(open systems interconnection)模型.
这里写图片描述
最下面的两层其实是硬件和驱动的领域,我们基本不会操作那块儿,除了会MTU的修改.网络层就是我们经常用到的V4和V6, 传输层我们通常用到的就是TCP和UDP,但是图中有个空隙表明有个叫raw socket的能绕过使用TCP和UDP和网络层直接读取数据链路层的数据.我们会在之后讨论.
那么为什要提供这个叫socket的东西呢?有两点原因:
1. 上层处理着所有应用层的任务并且基本上是不考虑交互的细节的.而下四层基本不清楚上层的应用,但是处理着交流的所有细节,比如发送数据, 等待ACK, 数据序列以保证数据的顺序性, J计算和验证checksum等.
2. 应用层就是处在我们通常所说的用户进程中,下四层处在内核进程中,所有提供这样的socket就是为了提供API.

1.7 BSD网络的历史

4.2BSD到4.4BSD都是由伯克利的CSRG(Computer Systems Research Group)研发而出的.但是由于ATT掌握着服务层的源码,所以BSD在1989年的时候研发出了第一款BSD网络操作系统. 并且开源.BSD的最后一个版本是1995年的4.4BSD-Lite2,它与4.4BSD-Lite就是构成了当今的BSD/OS, FreeBSD, NetBSD, OpenBSD. 这些延伸品不断的在更新加强中.好多的Unix系统都是开始于BSD网络操作系统的某个版本,包括socket API. 许多商业用途的Unix是基于System V Release 4(SVR4).
这里有一篇其他大哥写的文章相比较了BSD和System V:BSD VS SVR4,但是我们需要知道Linux这块Unix衍生品并不属于BSD领域的.它的网络代码和Socket API是白手起家的(from scratch).
这里写图片描述

1.8 测试网络和主机

这里写图片描述
在这套的学习中,我们只要就是使用这套网络拓扑图, 虽然在不同额网络下有不同的系统也有着不同的硬件,但是在显示的虚拟网络(VPN)和阿全shell(SSH)下,使得他们之间相连.
我们所要知道的就是两个基本的命令:netstat(8)和ifconfig(8)来获取我们的网络拓扑. 有的Unix产商会将这两个命令放置在管理员目录下/sbin或者/usr/sbin下而不是/usr/bin/,所有有可能在全全局的环境变量中没法直接执行这两命令.
netstat -i 提供了网络接口的信息 -n打印出地址.
NetBSD:
这里写图片描述
Ubuntu:
这里写图片描述
lo是表示回环接口,一般真机上会有以太网接口就是eth0. 我的是虚拟机.netstat -r是列出路由表.
NetBSD:
这里写图片描述
Ubuntu:
这里写图片描述
接下来给定接口的名字,我们就能通过ifconfig来查看接口的具体信息ifconfig eth0

1.9 Unix标准

Unix标准大部分是由奥斯丁大学的CSRG(Common Standards Revision Group)整理编辑. 大概有4000页覆盖了1700个接口函数.这些大多都在
Posix上指定了. 所以我们主要都回去讨论POSIX,除非有特殊的需求,才会看一些POSIX上没有,更古老的标准.
现在就解开POSIX的神秘面纱: POSIX(Protable Operating System Interface).它是IEEE的家族成员.POSIX标准也被ISO和IEC(ISO/IEC)采纳.
IEEE的1003.1-1988(317页)就是第一个POSIX标准.它指定了类Unix内核中的C语言接口, 并且覆盖了进程原语(fork, exec, signal和timers),进程的环境,文件和目录(所有的IO函数),终端IO, 系统的数据库, tar和cpio解压形式.
IEEE的1003.1-1990(356页), 也是ISO/IEC9945-1:1990. 细微的改变从88年版本到90年版本.
IEEE的1003.2-1992出来两卷,标题包含Shell和工具.这个部分定义了shell基于system V的sh和大约100个工具.我们称为POSIX.2
IEEE的1003.1b-1993就是IEEE P1003.4. 增加了文件的同步异步IO,信号量,内存管理, 调度操作, 时钟和timers, 还有消息队列.
IEEE的1003.1 1996版也叫做ISO/IEC 9945-1:1996. 增加了三章线程,线程的同步和锁还有线程的调度.我们称为POSIX.1.
IEEE的1003.1g增加了协议独立接口(PII)是2000年的标准.这个版本就是我们今后要主打的.
虽然现在的系统都是BSD和System V的衍生版本,但是随着POSIX.1, POSIX.2, Single Unix Specification Version3的普遍使用与各个产商,这样系统之间的差距也慢慢再缩小.主要的差异还存在于系统管理, 因为这块儿还没有出现标准化的规定.
IETF(The Internet Engineering Task Force)是一个大型的,开源的国际性的网络开发商,设计商,研究者,针对于网络的架构协议架构,网络流畅度等操作,他们处理的是协议问题而不是API.
在90年代开始就流行起了64位操作系统和64位软件, 其中一个原因是有着更大的进程空间地址.在32位机上的通用程序模型叫做ILP32, 64位机上的叫ILP64.
这里写图片描述
从我们编程者的角度看,在64位操作系统上,我们就不能再把指针当做整型存储了.我们必须考虑LP64再API上的影响了.
ANSI C开发出了size_t数据类型, 比如malloc中就会用到这样的参数: void *malloc(size_t size). 在32位系统上,size_t是个32位的值,但是在64位系统上,它必须是64位的值来处理更大的地址模型. 这就是意味着64位的操作系统必须要有一个typedef将size_t定义为unsigned long.
其中有个问题就是网络的API中的一些POSIX.1g指定一些函数参数有socket地址结构体大大小size_t(比如bind和connect的第三个参数). 一些XTI的结构中有一些成员有数据类型long.它们都需要从32位转成64位. 在这两个实例中,它们都不需要64位的数据类型,因为socket的地址结也就几百字节最多,所以XTI使用long类型就是个错误.解决这些问题的方式就是特殊指定对应特殊情形,socket API用socklen_t数据类型指定socket地址结构, XTI用t_scalar_t和t_uscalar_t数据类型.不将它们的值从32位转换成64位的原因是为了二进制兼容性在32和64位操作系统上.


联系方式: reyren179@gmail.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值