欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  网络运营

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

程序员文章站 2022-04-22 11:11:37
socket函数 指定期望的通信协议类型 socket()创建套接字,指定期望的通信协议类型; # include int socket(int family, i...

socket函数

指定期望的通信协议类型

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

# include

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

参数:

family指明协议族(协议域)

type指明套接字类型

protocal某个协议类型常值,或者设为0 返回值:

非负描述符(sockfd) – 成功,-1 – 出错

单纯调用socket函数:

- 指定了协议族和套接字类型

- 没有指定本地协议地址或远程协议地址

connect函数

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

# include

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

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

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

int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen)

第一个参数为监听套接字描述符

后两个参数都是(返回参数,就是我们传入后两个参数,后两个参数会将我们传入的信息(包含客户机地址信息的一个地质结构)记录在一个本地的地址结构里)客户端的信息,标识客户端的协议类型,IP地址,端口号。

(若对客户端信息不感兴趣,可以置空,也就是不记录)

为什么可以置空呢?如果我们在并发服务器上,有多个进程在accept,那如果不保留客户端信息,我们怎么知道该回给哪一个呢?

我暂时先瞎理解:accept函数是处理的客户端的SYN请求(从已完成连接队列中取出一个SYN分节),那么该分节里本身包含了客户端的源IP地址和端口号,所以即使置空,我们解析包的时候也能够提取到客户端的信息

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

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

fork和exec函数

fork函数:

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

# include

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

int close(int sockfd);

返回值:

成功为0

出错为-1

close将一个套接字标记为关闭,然后返回调用进程;

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

描述符引用计数

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

getsockname和getpeername函数

getsockname返回某个套接字的本机协议地址

getpeername返回某个套接字所关联的外地协议地址

返回在这里,是返回参数的意思,即将信息填充到参数指向的结构中

# include

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