UNP_4_基本TCP套接字编程

Posted on Mar 4, 2020

4.1 概述

  本章编写完整的TCP客户、服务程序所需要的套接字函数,并编写程序。同样包括并发服务器,通过派生子进程来处理新的连接

4.2 socket函数

调用socket函数来确定通讯协议的类型:使用 Ipv4 的 TCP,使用 Ipv6 的 UDP, Unix 域等等
#include <sys/socket.h>
int socket(int family, int type, int protocol);
//返回:成功为非负描述符,若出错则为-1

参数解析:famiy:代表协议族,type指明套接字类型
protocol设置为某个协议类型常值,或0,用来选择所选family和type组合的系统默认值

这里仅仅使用socket来获得一个套接字描述符,并给其绑定协议族和套接字类型,并没有指定本地协议地址和远程协议地址

  • AF_XX 和 PF_XX 的差别不做考虑

4.3 connect函数

TCP客户用connect来建立与TCP服务器的连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
//返回:成功返回0,出错返回-1

参数解析:sockfd是由socket返回的套接字描述符,第二个,第三个参数分别是一个指向套接字地址结构的指针和该结构的大小,套接字地址使用的是通用套接字地址结构,使用时需要进行类型转换,包含服务器的 IP + port

套接字地址结构必须含有服务器的IP地址和端口号,客户端调用connect不需要调用bind,因为内核会自动确定IP以及一个临时端口

TCP套接字在调用connect会进行三次握手过程,而且仅在建立连接成功的情况下或出错才会返回,出错返回有以下几种情况

  1. TCP客户没有收到SYN分节的响应,返回ETIMEDOUT错误,举例来说,发送一个SYN,无响应6s会重发,再无响应24s重发,再无响应75s再发一次,仍未收到则返回错误
  2. 若对客户的响应是RST(复位),则表示服务器在我们所访问的端口上并无进程等待与之连接,直接返回ECONNREFUSED错误 RST是TCP发生错误时返回的一种TCP分节,三种产生RST的方式为:
    1. 目的端口SYN到达,但并没有进程正在等待,返回RST
    2. TCP想取消一个已有连接
    3. TCP接收到一个根本不存在的连接上的分节
  3. 客户端发送的SYN在中间的路由上引发了“目的地不可达”ICMP错误,软错误,按第一种方式进行多次重发,如果仍未收到,则返回ICMP错误消息返回

connect 函数让客户端套接字从 CLOSED 状态,转换至 SYN_SEND 状态,三次握手成功后转换为 ESTABLISHED 状态。若失败,该套接字不可以使用,不可以在调用 connect。

4.4 bind函数

    bind函数把本地的协议地址赋予一个套接字。根据协议类型,可以是 32 位的 Ipv4 地址或 128 位的 Ipv6 地址加 16 位的 TCP 或 UDP 端口号
#include <sys/socket.h>
int bind(int sockfd, const struct *myaddr, socklen_t addrlen);

参数解析:第一个参数,是用来进行绑定的文件描述符,第二个参数是一个指向特定协议地址结构的指针,第三个参数是地址结构的长度

INADDR_ANY 是用来指定通配地址的常值,其值一般为 0,告知内核自己选择 IP 地址,Ipv6 使用 in6addr_any 来设置

服务器可以通过 bind 来绑定一个众所周知的端口,否则系统自动为其分配,但是这种情况十分少见,比如远程过程调用(RPC)
进程也可以默认绑定一个 IP,但必须是本地对外的端口
bind经常返回 EADDRINUSE 错误,表示地址已使用

4.5 listen函数

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

listen 仅由 TCP 服务器进行调用

  • 当 socket 创建一个套接字时,他被假设为一个主动套接字,它是一个将调用 connect 发起连接的客户套接字。listen 把一个未连接的套接字变为一个被动的套接字,指示内核应接收指向该套接字的连接请求。调用 listen 将套接字由 CLOSE 态转换为 LISTEN 状态。
  • 本函数的第二个参数用来指定队列最大个数
    • 未完成连接队列,由某客户端发送SYN到服务器,但还未完成三次握手过程,套接字处于STN_RCVD状态
    • 已完成连接队列,已完成三次握手,套接字处于ESTABLISHED状态 不同系统对这个参数有不同的解释,有加一,有加三等等

当来自客户端的 SYN 到达时,TCP 在未完成队列上创建一个连接,响应该 SYN,直到完成三次握手才会将其移动至完成连接队列的队尾,当服务器调用 accept 时,会从已完成队列的队首进行返回,为空则会睡眠,直到添加完成的连接才会唤醒。

不要设置为 0 ,不同系统解释不同,不想使用可以关闭套接字 如果收到一个 SYN 但是未完成队列已满,忽略它,并且不返回 RST,等待客户端重新发送

4.6 accept函数

accept函数由TCP服务进行调用,用于从已完成队列中返回下一个已完成连接,如果已完成队列为空,则投入睡眠

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

参数 cliaddr,addrlen 用来返回已完成连接的客户端的协议地址,addr_len 是值-结果参数
如果accept成功则返回一个由内核自动生成的描述符,代表与所返回的客户的链接
accept第一个参数为监听字套接符,称它的返回值为已连接套接字描述符
一个服务器同城仅仅创建一个套接字描述符,在他的生命周期中一直存在,内核为每个新的连接创建一个套接字描述符(TCP三次握手已经完成),当服务器完成对应的操作,相应的已连接套接字旧会关闭。
如果对客户端协议地址不感兴趣则可以将后两个参数置为空指针。

4.7 fork,exec函数

fock()函数,Unix中用来生成进程的唯一函数

#include<unistd.h>
pid_t fork(void); // 子进程中返回 0,父进程返回子进程 ID,出错为 -1

任何子进程仅有一个父进程,子进程可以通过调用 getppid 来获取父进程的 ID,父进程可以有许多子进程,他必须保存所有子进程的 ID,才可以进行跟踪子进程。
父进程调用 fork 前打开的所有描述符均和子进程进行共享,通过这可以将 accept 完成后在父进程关闭连接套接字,并在子进程中进行套接字的读写。

4.8 并发服务器

处理长时间的连接请求,并希望同时处理多个的情况下可以采用并发服务器,最简单的方式采用fork()来处理每一个客户

eg:
pid_t pid;
int listenfd,connfd;
// 服务器开始监听
listenfd = Socket();
Bind(listenfd, ...);
Listen(listenfd, LISTENQ);

for(;;){
    confd = Accept(listenfd, ...);
    if((pid = Fork()) == 0){
        // 子进程进行处理操作
        Close(listenfd);
        doit(connfd);
        Close(connfd);
        exit(0)
    }
    // 关闭套接字描述符
    Close(connfd);
}
  • 子进程完成服务后显式 close 描述符,其实没有这个必要,因为后面调用 exit 函数会关闭所有套接字描述符
  • 这里子进程关闭监听套接字,父进程则继续等待并关闭已连接套接字。(这里需要格外注意一下,每个套接字描述符均有一个引用计数,只有当引用计数为0时才释放描述符,所以父进程进行close时不会断开连接,因为子进程还在使用,引用计数 > 1)

4.9 close函数

通常Unix使用close来进行套接字的关闭,当引用计数为 0 时,会终止TCP连接。

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

close一个套接字默认行为是将其标记成以关闭,然后立即返回到调用进程,即该套接字不能再使用read,write操作

如果我们确实想关闭一个套接字(在其引用计数不为0的情况下)这里使用shutdown()函数即可实现

如果父进程(这里默认子进程结束后关闭描述符)对每个套接字均不进行close操作,最后会耗尽描述符的个数,更为重要的是,没有一个客户链接会被终止。

4.10 getsockname,getpeername函数

这两个函数或者返回与某某个套接字关联的本地协议地址,或者返回与某个套接字关联的外地协议地址。

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen *addrlen);
//二者成功均返回一,失败均返回0
  • 在一个不调用 bind 的 TCP 客户上,可以使用 getsockname 来获取内核分配的地址和端口号
  • 在一个调用 bind 但端口号为 0 的 TCP 客户上,可以使用 getsockname 来获取内核分配端口号
  • getsockname 可以用于获取某个套接字的地址族
  • 更多情景这里没有详解

小结

所有客户端和服务器端均从调用 socket 开始,大多数 TCP 服务器是并发的,为每个客户连接调用一个 fork。大多数 UDP 服务器是迭代的 服务器:socket->bind->listen->accept->close(shotdown) 客户端: socket->(bind)->connect->close(shutdown)