1. 参考书是Unix网络编程卷1

参考书第三章

这一章讲了什么事情?

套接字地址结构

sockaddr_in 这个结构体是IPV4专用的,里面有个字段是sin_addr,这是ipv4专用的地址结构体。

sockaddr 通用地址结构,我们后续学习bind的时候传递的参数就是它,也只能是它。因此不管是sockaddr_in还是别的什么,都要强制类型转换成这个类型。不用担心这样会丢失信息,因为family字段会保存地址族。

sockaddr_in6 和sockaddr_in是一类用途的,区别是这个地址结构是为了ipv6用的。和sockaddr_in的字段区别主要体现在scope和flowinfo上,这也是ipv6自己用的东西。

地址结构比较:

image.png
主要是说明ipv4和ipv6是固定长的,unix和datalink是可变长的。(指的是里面的数据可变长)对这些可变长的东西,len字段就尤为重要了。

值-结果参数

这一小节主要是讲传递套接字地址结构体的函数的设计理念。
有些时候,我们需要把套接字地址结构体传入内核,这样的函数有bind,connect和sendto。这三个函数的最大特点是传递的参数中,会传入结构体的大小。(就是说这三个函数都有个int之类的形参,用来表示传递的结构体的大小)
而从内核向进程传递时,我们传递的是指向代表结构体大小的变量的指针(int)。这样设计的目的是我们可以修改长度参数。当我们使用固定长度的套接字地址结构时,如in和in6,虽然我们有指针,但不会改变长度的值。但当我们使用变长的地址结构时,能得到新的长度就显得很重要了。因为这个长度通常是小于最大值的。
*(如果你写c++,并明白写C++时在参数里传一个非const的引用或者非指向常量的指针是为了什么,又或者你很熟悉linux系统的系统调用,那你一定能明白我在说什么。)

字节排序函数

计算机世界有大端法和小端法。网络协议用的是大端法,但主机用的可能是小端法(我记得intel都是小端法。书里给了检查大小端法的例程,真的是对union的非常巧妙的应用)。为了不出现错误,我们需要对数据进行转换。一共有四个函数,分别是ntohs ntohl htons htonl
n代表network,h代表host s和l代表short和long。

字节操纵函数

讲了一些函数。Berkeley系的字符串操纵函数和ansi c系列的字符串操纵函数。作者很贴心的教你怎么记这些参数的顺序。。。。。。唯一的坑在于memcpy函数处理src==dest的时候会出现不确定性为。

inet_aton inet_addr inet_ntoa

a代表ascii,理解成字符串
这三个函数用来进行点分十进制字符串,如”255.255.255.255”到32位二进制地址的转换。(n代表numeric)
中间那个不要用,已经被废弃了(事实上另外两个也不建议你用)

inet_pton和inet_ntop

大一统函数。你该用这个,将代码与特定协议解耦。

sock_ntop

inet_ntop将数值格式转换成表达格式。但数值格式是与协议高度相关的。如果是ipv4,你就需要sockaddr_in
如果是ipv6,你就需要sockaddr_in6。这样不好看,因此书里自己写了个包装函数。考试不用管这个。
(如果你看过CSAPP、APUE之类的书就知道这是传统艺能)

readn、writen和readline

也是书里定义的函数。如果你需要写很多字节(大于内核缓冲区的长度),一次write是不行的,需要多次write。read也同理。
总的来说,考试也不用管。

读完这一章有什么收获?

能分清sockaddr、sockaddr_in htons htonl pton ntop之类的函数是干嘛的了。。。。。。

参考书第四章

这一章讲了什么事情?

socket函数

在介绍socket函数之前,先看一看客户端/服务器编程模型(TCP)
image.png
//服务器端只三板斧-绑 听 接。
无论是客户端还是服务器端,最开始都需要调用socket()。
这个函数接收三个参数。第一个参数指明这个socket的协议族(如IPV4 IPV6等等),第二个参数指明传输方式(我个人的叫法,严格来说是套接字的类型。常用的就是STREAM和DGRAM,分别叫做字节流套接字和数据报套接字。一个TCP用,一个UDP用),第三个参数指明协议(TCP UDP SCTP)
这个函数的返回值是一个文件描述符,也就是socketfd。

connect函数

客户端连接服务器用的。
这个函数也有三个参数,分别是套接字的文件描述符、连接的地址(sockaddr之类的结构体)和表示连接的地址的结构体的长度。
这个函数如果成功返回0,否则返回-1。值得注意的是不能对connect失败的socket再次调用connect,必须关闭套接字,重新调用socket。

bind函数

bind函数有3个参数,分别是套接字的文件描述符,套接字地址结构和套接字地址结构的长度。其中,套接字
地址结构里(我假定是ipv4了)有ip和端口号。当然ip和端口号也都可以填写0。这样会由系统内核自己决定ip和端口号到底是什么。
当然,作为服务器端,通常来讲,我们最起码也得给个端口号(否则客户端怎么连接呢?)。ip我们通常也要给,因为一个子网下可能有多个ip。(注意,基于udp的话,必须bind(),基于tcp的话,如果光listen,不bind,也是可以的,在连接的时候进行bind。)
然而,bind函数的返回值只是0和-1,用来确定状态。如果我们让内核决定端口号是多少(虽然我们不会真的这么做),我们怎么知道他选了什么呢?需要用这个函数来知道内核选择了哪个端口->getsockname(返回的是一个套接字地址结构体,里面有端口)
客户端呢?客户端不需要bind。客户爱从哪ip哪端口来都行啊(手动狗头)。

listen函数

listen函数有两个参数,分别是文件描述符和backlog。它的作用是把主动套接字变成被动套接字(客户端是主动的,所以它不用listen)。参数backlog的含义是模糊的,它可以代表已完成连接队列+未完成连接队列的容量之和,也可以是未完成队列容量的2/3。这个数值很难给的好,一般来说对于当今时代的程序要给的大一点,原来给个5。
要问我的意见,就是这个函数不重要。。。。。。

accept函数

这个函数本身的返回值是正整数和-1。之所以不是0是因为它要返回新的套接字的文件描述符。参数比较有趣,分别是文件描述符,套接字地址结构和套接字地址结构的大小(地址结构和大小都是指针,dddd)。这个函数的第二和第三个参数会变化,用以返回套接字所连接的一个客户端的ip地址和端口号。
请注意,我们把传入accept函数的文件描述符对应的套接字叫做监听套接字,把返回的文件描述符对应的套接字称作已连接套接字。一个服务器程序通常只有一个监听套接字,但可以有无数个已连接套接字

fork和exec

fork出一个进程,然后让子进程执行另一个程序。大家都熟悉Linux,不多说了。

并发服务器

这里介绍了一个简单的基于多进程的并发服务器。

pid_t pid;
int listenfd,connfd;
listenfd = socket(……);
bind(listenfd,….);
while(1) {
connfd = accept(listenfd,&addr,&len); //后两个参数前面偷懒没写
if ( (pid = fork()) == 0) {
//child process
close(listenfd);
dosomething();
close(connfd);
exit(0);
}
close(connfd);
}

这里值得注意的是close(connfd),这一行会被父进程无条件的调用。
你可能有疑问:如果父进程关闭connfd的时候子进程没有结束dosth,那不就坏了吗?子进程怎么接着干活呢?
事实上不用担心,因为fork出来的子进程会增加对文件的引用计数。因此即使父进程关闭了connfd,connfd的引用计数也只是从2变成1,只有父子进程都结束,connfd对应的套接字文件才会被关闭。这里讲的很精彩,很清楚。

close函数

close函数会关闭一个套接字。被关闭的套接字不能再作为read或write的第一个参数。
如果父进程或者子进程忘记close已连接套接字,服务器父进程最终会耗尽文件描述符,此外,没有连接会被关闭!原因同上。

getsockname和getpeername

getsockname可以用来获取套接字的本地ip地址和端口号(不bind的话你需要通过这个函数来获取)。客户端调用因为之前没有bind过,可以获得内核分配的ip和端口号。服务端调用可以获得内核为这次连接分配的本地ip。
getpeername是服务器端子进程用的,可以获得对端ip地址和端口号。因为子进程通常会调用exec(telnet就是这样),调用exec之后,文件描述符还在子进程里,但之前父进程通过accpet获得的客户端的sockaddr没了。必须通过getpeername函数来获得客户的ip和端口号。
注意,虽然子进程有套接字的fd,但它不知道哪个fd是它需要的。我们有两种方式告诉他,一是把fd在fork的时候传进去,而是把fd固定到指定位置(把客户端套接字的fd变成子进程的0 1 2号fd),Telnet用的就是这种方式。
父进程因为在accept的时候就能获得这次连接的客户端套接字,因此不需要用getpeername。(这是我的个人理解)。

读完这一章有什么收获?

这一章是TCP客户端服务器模型的基石。介绍了建立和关闭连接需要用到的所有函数。
客户端只需要socket connect
服务端需要socket bind listen accpet
另外就是,每个connfd(也就是客户端套接字)都需要关闭。子进程每次需要关闭客户端套接字和监听套接字,父进程只关闭监听套接字。

参考书第五章:不用看

思考题:反转字节算法

image.png
实现logw的速度,因为每次都是二分的,只取一半,需要logw次操作。

额外内容
image.png
还有sendto和recvfrom,这两个函数多了两个参数,套接字地址结构体和长度。