【Unix网络编程读书笔记】第四章 基本TCP套接字编程
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
上一篇: C中字符串操作函数