UNIX网络编程(UNP) 第四章学习笔记

总括内容

我们不妨先来看下tcp客户端/服务端程序的套接字函数

我们可以看到服务端的起始到结束包含了 socket()->bind()->listen()->read()<->write()->close()

而客户端则是socket()->connect()->write()<->read()->close()

接下来,我们顺着这个调用顺序,从socket开始讲解tcp套接字函数

socket函数

函数定义

socket函数在<sys/socket.h>里面,函数定义为

int     socket(int family, int type, int protocol);

期中family是协议族,就是指示是使用IPv4还是IPv6或者一些更特殊的协议族,常用的主要是AF_INET,AF_INET6,分别表示IPv4和IPv6

type则是套接字类型,包括字节流套接字,数据包套接字等,常用的主要是SOCK_STREAM(常用于TCP),SOCK_DGRAM(UDP)

protocol是指特定的协议,因为family和protocol一般组合起来已经可以确定协议,所以可以填0表示使用默认值,不过也可以特别用来指定使用STCP等

常用套路
int tcp_fd=socket(AF_INET,SOCK_STREAM,0);//应用于IPv4协议采用tcp
int udp_fd=socket(AF_INET,SOCK_DGRAM,0);//应用于IPv4协议采用udp
具体解释

下面是family具体可取值以及解释

family 意义
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL unix域协议
AF_ROUTE 路由套接字
AF_KEY 秘钥套接字

下面则是type具体可取值以及解释

type 意义
SOCK_STREAM 字节流套接字,常用于TCP
SOCK_DGRAM 数据包套接字,常用于UDP
SOCK_SEQPACKET 有序分组套接字,常用于SCTP
SOCK_RAW 原始套接字,可以认为是IP层

我们之前提到两两组合的可能性,下面是特定的family和type组合后的结果,空格表示不可组合

AF_INET AF_INET6 AF_LOCAL AF_ROUTE AF_KEY
SOCK_STREAM TCP/SCTP TCP/SCTP 可用
SOCK_DGRAM UDP UDP 可用
SOCK_SEQPACKET SCTP SCTP 可用
SOCK_RAW IPv4 IPv6 可用 可用

下面是protocol的具体取值,可以看到就是tcp,udp和sctp

protocol 说明
IPPROTO_CP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP STCP传输协议
题外话

其实除了AF_之外,还有PF_,因为最早的时候,人们曾经想过一个协议族(PF)可以支持多个地址族(AF),然后PF用于创建套接字,AF用于创建地址,但实际时从来没有使用过,PF一般默认就是跟AF同值,不然可能会崩溃

connect函数

函数定义
int     connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen) ;

期中sockfd是从socket函数返回的文件描述符,servaddr则是目标服务器的地址,addrlen则是服务器长度

当然,你可以在connect之前调用bind去绑定一个ip地址,但是其实对于客户端来说,调用connect的时候系统就会识别IP地址,并且自动指定一个端口给你了

常用套路
if ((status=connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))<0){}//connect返回如果小于0,代表着发生了错误
具体解释

connect会触发三次握手,然后只有在成功或者出错的时候返回,出错的原因可能有:

  1. 如果TCP客户端没有收到SYN的响应(就是三次握手中的ACK),那么就会触发ETIMEOUT,具体来说,调用CONNECT的时候,会先发一次SYN报文,没有响应的话,等待6秒之后再发一次报文,如果还没有响应,那么等待24秒之后发第三次SYN报文,最后等待总共75s,如果还没有就返回ETIMEOUT
  2. 如果SYN报文获得的响应是RST报文,代表着对端根本没有正在等待的进程,这是一种硬错误(hard error),此时会立即触发ECONNREFUSED
  3. 如果SYN报文获得的响应是"destination unreachable"的ICMP报文,那么系统认为这是软错误,内核会保存这条信息,然后按照第一种情况描述的时间间隔继续发送SYN报文,然后也是在规定时间(BSD规定为75s)之后如果没有收到响应,那么就会将保存的ICMP错误作为EHOSTUNREACH或者ENETUNREACH错误发送回进程
题外话
  • 之前提到如果收到RST报文就xxx,那么什么情况下会收到RST报文呢?

    1. 如上所述,SYN报文已经达到了服务器,但是没有对应的进程在监听
    2. 服务器想要取消这次连接
    3. TCP服务器有监听,但是这个请求是来自于根本不存在的连接(比如说之前有一个连接发送了分包迷路了,然后好不容易找回来,这时候连接已经不在了)
  • 会因为什么情况收到ICMP报文呢?

    1. 按照本地的转发表,目标是不可达的
    2. connect调用不等待就返回
  • 虽然我们说会返回ENETUNREACH,但是其实网络不可达的信息一般认为已过时,我们一般只会返回EHOSTUNREACH

    看起来名字很冗长,其实就是E-HOST-UNREACH 以及E-NET-UNREACH

  • 当我们使用socket函数之后,套接字处于CLOSED状态,调用connec的话,会进入SYN_SENT状态,如果成功的话,套接字会进入ESTABLISHED状态,但是如果失败了,那么这个套接字就不能再用connect了,必须close掉然后重新connect

  • 具体来说,如果ASN到达服务器,只是没有对应的进程,就会返回RST报文(第二种情况),如果在过程中发现不可达,那么就返回ICMP报文,如果什么事情都没有发生,只是单纯的(可能因为丢包什么的)而没有消息,就返回timeout

实战尝试

首先我们要祭出一个用来访问13端口(获取时间)的client,这是第二章里面的内容,然后,我们可以开始模仿上述条件

  1. 当访问得不到响应的时候,触发Timeout(等待的时间会比较长,约75s)

    一个简单的方法,是请求连接一个子网上没有的节点比方说(192.168.1.100),然后arp试图找到相应硬件失败就会触发了

  2. RST报文

    空缺

  3. ICMP

    空缺

bind函数

函数定义
int     bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);//成功返回0,否则返回-1

bind会试图讲一个本地协议地址与套接字绑定,本地协议地址是 32位的ipv4地址/128位的ipv6地址+16位的UDP/TCP端口号

常用套路
struct sockaddr_in servaddr; 
bzero(&servaddr, sizeof(servaddr)); //初始化servaddr
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //表示通配IP地址
servaddr.sin_port = htons(13); //表示绑定13端口(这是周知端口,用来汇报时间)
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));//绑定
具体解释
  • IP地址的选择可以是通配的(如上述htonl(INADDR_ANY)),在这种情形下,对于客户端来说,内核会根据所用的外出网络接口来确定使用的IP地址,如果是服务器,内核则会将SYN报文中指定的目的IP地址作为服务器的源IP地址(注意这里面说的概念,你可以认为一个主机可以有多个IP地址,比方说服务器能收到SYN报文,说明服务器的确有这么个IP)

    当然也可以选择绑定主机有的网络接口的IP地址,在这种情形下,对于客户端来说,就相当于指定了自己的IP地址(一般来说客户端不需要绑定),对于服务端来说,就限定了这个套接字接受连接的范围

  • port也同样是可以通配的(通过指定0来实现),事实上,无论是客户端还是服务器,如果在调用connect和listen之前没有调用bind函数,内核都会随机指定一个端口,这对于客户端来说是可以接受的,但是服务端一般不可以,因为服务端依赖于周知端口来被人所认识(不然客户端不知道该用什么样的端口来访问服务端)

    一个例外是rpc服务器

  • 如果让内核指定随机端口,bind的返回值并不指示端口号码,我们要用getsockname()来返回协议地址

  • 如果端口随机,那么会在调用bind的时候分配一个随机端口,但是如果IP地址随机,那么会在第一次成功建立连接(TCP)或者在套接字上成功发出数据报的时候才确定(UDP)

listen函数

函数定义
int     listen(int sockfd, int backlog);

listen函数主要完成两件事情:

  1. 经过socket函数得到的套接字默认是被动套接字(客户端),不能接受请求,调用listen可以将被动套接字转换成主动套接字(从Closed状态转移为Listen状态)
  2. listen还会指定backlog数字
常用套路
Listen(listenfd, LISTENQ);// 这里面LISTENQ是作者指定的1024,因为本着如果不支持这么大的backlog,那么内核会裁切的原则,如果检索会发现,很多实现设置为128
具体解释

要理解backlog的含义,我们需要理解,如果我们回顾三次握手,我们会注意到服务器接收到SYN报文然后发送ACK并且进入SYN_RCVD状态,与最后收到ACK进入ESTANBLISHED状态,是有时间差的,因此当前的实现,是用两个队列,一个存储的是进入SYN_RCVD的套接字,我们称之为未完成连接队列,一个存储的是进入ESTANBLISHED状态的套接字,我们称之为已完成连接队列

当服务器收到一个连接请求的时候,就会将该连接加入未完成连接队列,等待如果得到ACK之后,会将该项转移到已完成连接队列

那么,backlog代表的是什么呢?

backlog是已完成连接队列的最大长度,当然有那么一段时间表示的是两个队列总和长度,而且当前的实现都提供了一个“模糊因子“,这是因为需要为未完成连接队列提供额外的长度

当队列已满的时候,新的SYN报文会被丢弃(但是不会发送RST报文),这是因为逻辑上,允许客户端过一段时间重发看是否还有没有满

当三次握手连接完成之后,服务器调用accept之前的数据,会被存储在已连接套接字的接收区缓冲区

accept函数

函数定义
int     accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

注意的是,我们这里有两个套接字概念,第一个是accept中的第一个参数,同时来自于socket->listen,称为监听套接字,第二个套接字来自于accept返回的套接字,称之为连接套接字(这是针对客户的)

cliaddr和addrlen是用于返回给调用者client信息的,如果不需要可以设置为NULL,注意addrlen是典型的值-结果参数

常用套路
struct sockaddr_in cliaddr;
socklen_t len;
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &len);
printf("connection from %s,port %d \n", inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),ntohs(cliaddr.sin_port));

fork函数

函数定义
pid_t	 fork(void);

fork会创建一个新的子进程,一个比较迷惑人的地方在于,fork有两个返回值,对于父进程来说,fork的返回值是创建的子进程的pid,而对于子进程来说,返回的是0,因此一般我们可以用返回值来区分执行逻辑

常用套路
pid_t pid;
if ((pid=fork())<0)
{
    //error
}else if (pid==0){
    //子进程逻辑
    print("Hello world");
    exit(0);//注意这句话只会停止执行子进程,因为只有子进程会进入这个条件分支,用exit的办法,那么父进程就不需要嵌套在else里面(来避免子进程执行)
}
//父进程逻辑
具体解释
  • fork创建子进程的话一般可能有两种使用用途:

    1. 创建一个相关的副本,从而互不干扰的执行

    2. 一个进程想要执行另外一段程序,用fork执行之后,在调用exec来把自身替换为某个程序,常用于shell中

      exec有6个相关函数,主要区别是比方说提供的文件名还是路径名,传入命令行参数是怎么做的之类,只有execve是系统调用

  • fork之前打开的所有描述符都会被子进程共享,所以一般来说我们可以fork一个子进程,然后子进程继续处理已连接套接字,而父进程关闭了该已连接套接字

实战多进程服务器

关键代码
for (;;)
    {
        // 返回的是已连接描述符,用于与连接的客户通信
        connfd = Accept(listenfd, (SA *)&cliaddr, &len);
        if (fork() == 0) //if 里面只有子进程会运行
        {
            Close(connfd);//因为监听套接字在子进程是不必要的
            printf("connection from %s,port %d \n", inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
                   ntohs(cliaddr.sin_port));
            ticks = time(NULL);
            snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
            Write(connfd, buff, strlen(buff));
            Close(connfd);
            exit(0);
        }
        Close(connfd);//因为父进程不操心连接套接字
    }
具体解释

==0的做法在之前已经提到,这样我们可以区分”子进程运行逻辑“和”父进程运行逻辑“

不过可能很多人会好奇,我们之前提到过,如果我们对一个套接字调用close会导致系统发送FIN报文,然后进入正常关闭逻辑,但是我们却在父进程显示close了连接套接字,这难道不会导致套接字关闭吗?

为了明白这点,我们需要理解,每个文件(I/O)和套接字,都会维护一个引用计数,他是当前打开着的引用该文件或者套接字的描述符的个数

一般来说,exit会关闭所有打开的文件描述符,不过可能会有人倾向于显式关闭

close函数

函数定义
int	 close(int fd);
详细解释

当只有fd引用文件或者套接字的时候,close指定会标记该套接字为关闭并立即返回(不阻塞),描述符将不再能够用于read/write函数的第一个参数,对于tcp来说,会试图继续发送所有已经排队准备发送的数据,然后发生正常的关闭序列(就是fin报文什么的)

由于close不一定能够关闭描述符(子进程持有),所以也可以调用shutdown来替换close

close函数在服务器中尤其重要,因为首先,如果父进程不关闭,那么很可能会出现描述符耗尽的可能性,其次是如果父进程不关闭,那么连接套接字将永远不被关闭

getsockname与getpeername函数

函数定义
int     getsockname(int sockfd, struct sockaddr * localaddr, socklen_t * addrlen);
int     getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t * addrlen);

期中getsockname返回的是本机在该套接字上的地址信息,而getpeername返回的是对端在该套接字上的地址信息

详细解释

其实调用起来很简单,不过我们需要注意的是,如果是服务端,那么应该传入的sockfd应该是连接套接字而不是监听套接字

发布了31 篇原创文章 · 获赞 32 · 访问量 746

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/103193972
今日推荐