LINUX 基本套接字函数

    • 概述
    • socket函数
    • connect函数
    • bind函数
    • listen函数
    • accept函数
    • fork函数
    • 并发服务器
    • close函数
    • getsockname和getpeername函数

概述

图4-1给出了在一对TCP客户与服务器进程之间发生的一些典型事件的时间表。服务器首先启动,稍后某个时刻客服启动,它试图连接到服务器。我们假设客户给服务器发送一个请求,服务器处理该请求,并且给客户发回一个响应。这个过程一直持续下去,直到客户关闭连接的客户端,从而给服务器发送一个EOF(文件结束)通知为止。服务器接着也关闭连接的服务器端,然后结束运行或者等待新客户连接。


注意:struct sockaddr

#define SA struct sockaddr

也就是通用套接字地址结构.每当一个套接字函数需要一个指向套接字地址结构的指针时,这个指针必须强制类型转成一个指向通用套接字地址结构的指针。这是因为套接字函数早于ANSI C标准,当时void *指针类型还不可用.

socket函数

#include 
/* 返回:若成功则为非负描述符,若出错则为-1 */
int socket(int family, int type, int protocol);
  • family参数指明协议族,它是图4-2中所示的某个常数。该参数也往往被称为协议域。
  • type参数指定套接字类型,它是图4-3中所示的某个常数值。
  • protocol参数应设为图4-4所示的某个协议类型常值,或者设为0,以选择family和type组合的系统默认值。
family 说明
AF_INET IPv4协议
AF_INET6 IPv6协议
type 说明
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
protocol 说明
IPPROTO_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

connect函数

TCP客户用connect函数来建立与TCP服务器的连接。

#include

/* 
 * 返回:若成功则为0,若出错则为-1 
 * 
 * 第二个参数,第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。
 * 套接字地址结构必须包含有服务器的IP地址和端口号。
 */
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

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

如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在成功或错误时才退出,其中出错返回可能有以下几种情况:

  1. 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。
  2. 若对客户的SYN响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没有运行),返回ECONNREFUSED错误。产生RST的三个条件是:目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;TCP想取消一个已有连接;TCP接收到一个根本不存在的连接上的分节。
  3. 客户发出的SYN在中间的某个路由器上引发目的地不可达。客户主机内核保存该消息,并增大间隔时间重发SYN。若在某个规定的时间后仍未收到响应,则把保存的消息(ICMP错误)作为EHOSTTUNREACH或ENETUNREACH错误返回给进程。以下两种情形也是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径,二是connect调用根本不等待就返回了。

bind函数

bind函数把一个本地协议地址赋予给一个套接字。

#include  

/*
 * 返回: 若成功则为0,若出错则为-1
 * 
 * 第二个参数是一个指向特定的地址结构的指针
 * 第三个参数是该地址结构的长度。
 */
 int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以不指定。如果哦服务器指定,则只接受那些目的地为该IP地址的客户连接。

IP地址 端口 结果
通配地址 0 内核选择IP地址和端口
通配地址 非0 内核选择IP地址,进程指定端口
本地IP地址 0 进程指定IP地址,内核选择端口
本地IP地址 非0 进程指定IP地址和端口

需要注意的是,如果让内核来为套接字选择一个临时端口,函数bind并不返回所选择的值,必须调用getsockname来返回协议地址。

listen函数

#include 

/*
 * 返回: 若成功则为0,若出错则为-1
 *
 * 第二个参数规定了内核应该为相应套接字队列的最大连接个数。
 * http://man7.org/linux/man-pages/man2/listen.2.html
 * The backlog argument defines the maximum length to which the queue of
 *   pending connections for sockfd may grow.
 */
 int listen(int sockfd, int backlog);

如何理解backLog参数?我们必须认识到内核为任何一个给定的监听套接字维护2个队列:
LINUX 基本套接字函数_第1张图片
LINUX 基本套接字函数_第2张图片

  1. 未完成连接队列:每个这样的SYN分节对应其中一项,已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
  2. 已完成连接队列:每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。
  3. 每当在为完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。这种机制完全自动,无需服务器进程插手。
  4. 在未完成连接队列中连接,若在RTT时间内还未完成三次握手,则超时并从该队列中删除。
  5. 当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
  6. 当一个客户SYN到达时,若这些队列满了,TCP应该忽略这些该字节,而不应该发送RST。因为:这种情况时暂时的,客户TCP将重发SYN,期望不久就能在这些队列中有可用的空间。要是服务器TCP立即响应一个RST,客户的connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区别响应SYN的RST究竟意味着“该端口没有服务器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了”。

accept函数

accept函数由TCP服务器调用,用于从已完成连接队列头返回下一个已完成连接(图4-7)。如果已完成队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞模式)。

#include 

/*
 * 返回:若成功则为非负描述符,若出错则为-1
 *
 * 参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。
 * addrlen是值-结果参数:
 *     调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,
 *     返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。
 * 
 * accept的第一个参数为监听套接字,返回值为已连接套接字。 
 * 
 * 本函数最大返回三个值:
 *   一个即可能是新套接字描述符也可能是出错指示的整数,客户进程的协议地址(由cliaddr指
 *   针所指)以及该地址的大小(由addrlen指针所指)。如果对返回的客户地址不感兴趣,那么
 *   可以把cliaddr和addrlen均置为空指针。
 */
 int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
#include    "unp.h"
#include    

int
main(int argc, char **argv) {
    /*
     * len: 它将成为一个值-结果变量;
     * cliaddr: 它将存放客户的协议地址
     */
    int listenfd, connfd;
    socklen_t len;
    struct sockaddr_in servaddr, cliaddr;
    char buff[MAXLINE];
    time_t ticks;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    /*
     * 下面情况都是在代码正常的情况下进行修改。
     *
     * 1. 注释掉 Listen(listenfd, LISTENQ);
     *    错误提示为:
     *          1)没有sudo权限时,提示:bind error: Permission denied。因为13端口是保留端口
     *          2)给予权限时,提示:accept error: Invalid argument
     *
     * 2. 将端口号从13该为9999后,注释掉Listen(listenfd, LISTENQ);
     *    错误提示:
     *          1)accept error: Invalid argument。并没有出现权限的错误。
     *
     * 3. 删掉bind,保留listen
     *    可以编译以及运行成功,但是不能正确的接收到请求。
     */
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(13);    /* daytime server */

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    for (;;) {
        /*
         * 我们将len初始化为套接字地址结构的大小,将指向cliaddr结构的指针和指向len的
         * 指针分别作为accept的第二和第三个参数.调用inet_ntop将套接字地址结构中的32位
         * IP地址转换为一个点分十进制ASCII字符串,调用ntohs将16位的端口号从网络字节序转
         * 换为主机字节序
         */
        len = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *) &cliaddr, &len);
        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);
    }
}

fork函数

#include 

/*
 * 返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
 *
 * 在子进程中可以通过getppid取得父进程的进程ID,则也就是为什么返回0的原因。
 * 而在父进程中,可能有很多的子进程,所有返回子进程的ID
 * 
 * 父进程中调用fork之前打开的所有描述符在fork返回之后有子进程共享。
 */
 pid_t fork(void);

fork有两个典型用法:

  1. 一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时各自的某个操作。这是网络服务器的典型用户。
  2. 一个进程想要执行另一个程序。既然创建新进程的唯一方法(?)是调用fork,该进程于是首先调用fork创建一个自身的副本,然后在其中一个副本(通常为子进程)调用exec(这里并不介绍)把自身替换成新的程序。这是诸如shell之类程序的典型用法。

并发服务器

# 典型的并发服务器轮廓
pid_t pid;
int   listenfd, connfd;
listenfd = Socket( ... );
Bind(listenfd, ... );
Listen(listenfd, LISTENQ);

for ( ; ; ) {
    connfd = Accept(listenfd, ... );
    if ((pid = Fork()) == 0) {
        Close(listenfd);
        doit(connfd);
        Close(connfd);
        exit(0);
    }
    Close(connfd);
}

父进程对connfd调用close没有终止它与客户的连接呢(先于子进程执行)?为了便于理解,我们必须知道每个文件或套接字都有一个引用计数。应用计数在文件表项中维护,它是当前打开着的引用该文件或套接字的描述符的个数。socket返回后与listenfd关联的文件表项的引用计数值为1。accept返回后与connfd关联的文件表项计数也为1。然而fork返回后,这两个描述符就在父进程与子进程间共享(也就是被复制),因此与这个两个套接字相关联的文件表项各自的访问计数值均为2。这么一来,当父进程关闭connfd时,它只是把相应的引用计数从2减为1。该套接字真正的清理和资源释放要等到其引用计数值到达0才发生。这会在稍后子进程也关闭connfd时发生。

close函数

#include 

/*
 * 返回:若成功则为0,若出错则为-1
 * 
 * int close(int sockfd);
 */
 int close(int sockfd);

如果引用计数值仍大于0,这个close调用并不引发TCP的四分组连接终止序列。但是我们确实想在某个TCP连接上发送一个FIN,那么可以改用shutdown函数代替close。

如果父进程对每个由accept返回的已连接套接字都不调用close,那么并发服务器中将会发生什么。首先,父进程最终将耗尽可用描述符,因为任何进程在任何时刻可拥有的打开着的描述符通常是有限制的。不过更重要的是,没有一个客户连接会被终止。当子进程关闭已连接套接字时,它的引用计数有2递减为1且保持为1,因为父进程永不关闭任何已连接套接字。这将妨碍TCP终止序列的发生,导致连接一直打开的。

getsockname和getpeername函数

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

#include 

/*
 * 均返回:若成功则为0,若出错则为-1
 * 
 * 这两个函数的最后一个参数都是值-结果。
 */
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen); 
  • 在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号。
  • 在已端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号。
  • getsockname可用于获取某个套接字的地址族。
  • 在一个以通配IP地址调用bind的TCP服务器上,与某个客户的连接一旦建立(accept返回),getsockname就可以用于返回由内核赋予该连接的本地IP地址。
  • 当一个服务器是由调用accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径便是调用getpeername。
#include    "unp.h"

int
sockfd_to_family(int sockfd) {
    /*
     * 既然不知道要分配的套接字地址结构的类型,我们于是采用sockaddr_storage这个
     * 通用结构,因为它能够承载系统支持的任何套接字地址结构
     */
    struct sockaddr_storage ss;
    socklen_t len;

    /*
     * 我们调用getsockname返回地址族。既然POSIX规范运行对未绑定的套接字调用
     * getsockname,该函数应该适合任何已打开的套接字描述符。
     */
    len = sizeof(ss);
    if (getsockname(sockfd, (SA *) &ss, &len) < 0)
        return (-1);
    return (ss.ss_family);
}

你可能感兴趣的:(linux,网络编程)