【Unix网络编程读书笔记】第四章 基本TCP套接字编程

时间:2022-09-26 10:59:33

socket函数

指定期望的通信协议类型
socket()创建套接字,指定期望的通信协议类型;

# include <sys/socket.h>
int socket(int family, int type, int protocal);
  • 参数:
    family指明协议族(协议域)
    type指明套接字类型
    protocal某个协议类型常值,或者设为0
  • 返回值:
    非负描述符(sockfd) – 成功,-1 – 出错

单纯调用socket函数:
- 指定了协议族和套接字类型
- 没有指定本地协议地址或远程协议地址

connect函数

TCP客户用于建立与TCP服务器的连接,可以理解为发送SYN

# include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
  • 参数:
    sockfd: socket函数返回的一个套接字描述符
    servaddr: 一个指向套接字地址结构的指针(该结构包括IP地址和端口号)
    addrlen: 该结构的大小
  • 返回值:
    若无错误发生,则connect()返回0。
    否则的话,返回SOCKET_ERROR错误

客户在调用connect函数之前不必非得调用bind函数,需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。

错误返回:

  • 若TCP客户没有收到SYN分节的相应,则返回ETIMEDOUT错误
  • 若客户收到RST,表明服务器上没有进程等待与之连接(如服务器进程没在运行)。这是一种硬错误(hard error),用户已接受到RST就马上返回ECONNERFUSED错误
  • 若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”的ICMP错误,则认为是一种软错误(soft error)。客户按照时间间隔继续发SYN,如果在规定时间内还没有得到响应,则返回EHOSTUNREACH或ENETUNREACH给客户端进程。
    引发该错误的两种原因,1是按照本地转发表到不了服务器的路径,2是connect调用根本不等待就返回。

如果connect失败,则要close当前的sockfd,并且重新调用socket函数创建新的套接字

bind函数

bind函数将一个本地协议地址赋予一个套接字。
协议地址: 32位的IPv4地址或128位的IPv6地址 + 16位TCP/UDP端口号

# include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen)

参数表和connect很像,同一个sockfd,先bind本机地址,再connect对端地址
后两个参数,可以指定一个,也可以不指定,如上述:客户在调用connect函数之前不必非得调用bind函数,需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。

客户机:IP地址为源IP地址
服务器:IP地址意味着服务器只接受那些目的地为这个IP地址的客户机连接

客户机通常不绑定IP地址到套接字,而是建立连接时,内核将根据所用的外出网络接口来选择源IP地址。
如果服务器没有绑定IP地址,则选用收到的客户机的SYN请求的目的地作为服务器的源IP地址

如果两者都不指定,则设置IP地址为通配地址,端口号为0

如果想要知道内核选择的临时的端口值,必须调用getsockname

返回值:成功为0,不成功为-1

bind常见的返回错误为EADDRINUSE(Address already in use)地址已使用

listen函数

仅由TCP服务器调用,做两件事情:
1. listen函数将一个未连接的主动套接字转换为被动套接字(监听套接字),将CLOSE状态转换到LISTEN状态。
2. 第二个参数规定了内核应该为相应套接字排队(见下)的最大连接个数

# include <sys/socket.h>
int listen(int sockfd, int backlog);

返回值:
若成功返回0,若出错则为-1

内核为任何一个监听套接字维护两个队列(队列里存的是SYN分节)

  • 未完成连接队列(incomplete connection queue)
    每个这样的SYN分节处于TCP三次握手过程中,处于SYN_RCVD状态
  • 已完成连接队列(completed connection queue)
    已经完成TCP三次握手,处于ESTABLISHED状态

一般来说,两个队列之和不超过backlog

如果未完成序列满了之后,TCP客户端发送一个SYN分节,服务端不响应,也不发送RST,让TCP期望下一次重传,有可能未完成序列会有位置

在此理解SYN洪泛攻击就比较清楚了。预防的一种方法是,我们将backlog指定为某个给定套接字上内核为之排队的最大已完成连接数。这样就不必为了提供SYN洪泛的防护而设定一个很大的backlog值。

accept函数

由TCP服务器调用,用于从已完成连接队列队头返回一个已完成连接

# include<sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen)

第一个参数为监听套接字描述符
后两个参数都是(返回参数,就是我们传入后两个参数,后两个参数会将我们传入的信息(包含客户机地址信息的一个地质结构)记录在一个本地的地址结构里)客户端的信息,标识客户端的协议类型,IP地址,端口号。
(若对客户端信息不感兴趣,可以置空,也就是不记录)
为什么可以置空呢?如果我们在并发服务器上,有多个进程在accept,那如果不保留客户端信息,我们怎么知道该回给哪一个呢?
我暂时先瞎理解:accept函数是处理的客户端的SYN请求(从已完成连接队列中取出一个SYN分节),那么该分节里本身包含了客户端的源IP地址和端口号,所以即使置空,我们解析包的时候也能够提取到客户端的信息

若成功返回非负描述符(已连接套接字描述符),不成功返回-1。

一个服务器(个人觉得是对于一个服务,不保证一个服务器上不同的IP地址和端口号可以处理不同的服务)通常只有一个监听套接字,然后内核为每个由服务器进程接受的客户创建(通过accept函数)一个已连接套接字, 当服务完成的时候,相应的已连接套接字关闭。

fork和exec函数

fork函数:

是Unix中派生新进程的唯一方法。

# include <unistd.h>
pid_t fork(void);

返回值:
在子进程中为0
在父进程中为子进程ID
出错为-1

父进程调用accept之后调用fork,accept创建的已连接套接字与fork出的子进程共享,之后,子进程继续读写这个已连接套接字,父进程关闭这个已连接套接字。

fork两个典型用法:
1. 一个进程创建自身的副本,然后两个进程并发执行
2. 一个进程想要执行另一个程序。先fork出自身的一个副本,然后副本调用exec函数,把自身替换成新的程序。

exec函数

有6个exec函数,统称为exec函数。
放在硬盘上的可执行文件被Unix执行的唯一方法是:由一个现有进程调用6个exec函数中的某一个,把当前的进程映像替换成新的程序文件,而且新程序同main函数开始执行,进程ID不改变。
调用exec的进程叫做 调用进程
新执行的程序为 新程序

并发服务器

我觉得这段用书上的代码解释应该非常清楚:
都用了包裹函数

...
Listen(listenfd, backlog);
for (;;) {
connfd = Accept(listenfd, ...);
if ( (pid = Fork()) == 0 ) { //成功创建子进程
Close(listenfd); //子进程关闭监听套接字, 父进程可以继续监听
doit(connfd); //子进程在已连接套接字上读写
Close(connfd); //完成与客户机的交互,断开连接
exit(0);//正常退出
}
Close(connfd); //父进程关闭已连接套接字
}
...

但是我觉得如果做到并发的话,第6~9行和第11行应该同时执行。

close函数

Unix中close函数也用来关闭套接字,断开TCP连接

# include <unistd.h>
int close(int sockfd);

返回值:
成功为0
出错为-1

close将一个套接字标记为关闭,然后返回调用进程;
被标记为关闭的套接字不能再由调用进程使用,也就是不能再作为read和write的第一个参数。

描述符引用计数

通俗理解的话:并发中,fork会让对应的套接字引用计数加1,close函数会让对应的套接字引用计数减1,该计数被父进程和子进程共享(可读写),只有当该计数为0时,才会终止TCP连接,4次挥手。

getsockname和getpeername函数

getsockname返回某个套接字的本机协议地址
getpeername返回某个套接字所关联的外地协议地址
返回在这里,是返回参数的意思,即将信息填充到参数指向的结构中

# include<sys/socket.h>
int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t* addrlen);
int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t* addrlen);

返回值:
均为若成功:返回0
若失败:返回-1

用途:

  • 没有显式bind时,connect成功后,getsockname用于返回内核赋予该连接的IP地址和端口号;
  • bind时端口号参数为0时,connect成功后,getsockname用于返回内核赋予该连接的本地端口号;
  • getsockname用于获取套接字地址的地址族
  • 服务器采用通配地址bind时,对已连接套接字调用getsockname也可以得到IP地址和端口号
  • 服务器通过调用accept的进程通过exec执行程序时,获取客户身份的唯一途径是getpeername。
  • Telnet服务器首先调用的函数之一就是getpeername