传输层TCP协议——TCP套接字编程

  • socket()              创建套接字
  • bind()                  绑定本机地址和端口
  • connect()            建立连接
  • listen()                设置监听套接字
  • accept()              接收TCP连接
  • recv(), read(), recvfrom()        数据接收
  • send(), write(), sendto()          数据发送
  • close(), shutdown()                  关闭套接字

bind:

bind函数把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。
#include 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数参数

  • sockfdsocket:调用返回的文件描述符
  • addr指向特定于协议的地址结构的指针,包含本机IP地址和端口号
  • addrlen地址结构的长度

返回值成功 0    出错 -1


bind函数指定要捆绑的IP地址和/或端口号:

进程指定

结果

IP地址

端口

通配地址

0

内核选择IP地址和端口

通配地址

非0

内核选择IP地址,进程指定端口

本地IP地址

0

进程指定IP地址,内核选择端口

本地IP地址

非0

进程指定IP地址和端口

  • 如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。
  • 如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址。
IPv4:通配地址由常值INADDR_ANY指定,其值为0
struct sockaddr_in addr;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
IPv6:IPv6地址存放在结构中,系统预先分配变量in6addr_any的extern声明并将其初始化为常值IN6ADDR_ANY_INIT(extern const struct in6_addr in6addr_any;  /*::*/)
struct sockaddr_in6 addr;
addr.sin_addr = in6addr_any;
typedef struct sockaddr SA;

//定义一个struct sockaddr_in类型的变量并清空
listenfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in myaddr;
bzero(&myaddr, sizeof(myaddr));

//填充地址信息
my_addr.sin_family = PF_INET;
my_addr.sinport = htons(8888);
my_addr.sin_addr.s_addr = inet_addr(“192.168.1.100”);

//将my_addr强制转换为struct sockaddr类型在函数中使用
int status = bind(listenfd, (SA *)&my_addr, sizeof(my_addr));

connect:

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

#include 
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
函数参数:
  • sockfd                 socket返回的文件描述符
  • servaddr            服务器端的地址信息,端口和IP地址
  • addrlen               serv_addr的长度

返回值:成功 0 出错 -1

typedef struct sockaddr SA;
int status = connect(sockfd, (SA *)&addr, sizeof(addr));
  • connect()是客户端使用的系统调用,客户在调用connect函数前不必非得调用bind函数,如果需要,内核会确定源IP地址,并选择一个临时端口作为源端口。
  • 对于TCP套接字,调用connect函数将激发TCP的三路握手过程,仅在连接建立成功或出错时才返回。
出错返回可能有以下几种情况:
  • 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误
  • 若对客户的SYN响应是RST,表明服务器主机在我们指定的端口上没有进程在等待与之链接(服务器进程也许没在运行)。这是一种硬错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误。
  • 若客户发出的SYN在中间的某个路由器上引发一个”destination unreachable“ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该ICMP错误消息,并按一定时间间隔继续发送SYN。若在某个规定的时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH或ENETUNREACH错误返回给进程。
tcp_connect函数:

执行TCP客户端的通常步骤:创建一个TCP套接字并连接到一个服务器

int tcp_connect(const char *hostname, const char *service);
返回值:成功则返回已连接套接字描述符,出错不返回

int tcp_connect(const char *host, const char *serv)
{
    int sockfd, n;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof (struct addrinfo)) ;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((n = getaddrinfo (host, serv, &hints, &res)) != 0)
        err_quit("tcp_connect error for %s, %s: %s",host, serv, gai_strerror(n)) ;
    ressave = res;

    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue;		/* ignore this one */
        if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
            break;			/* success */
        close (listenfd);		/* ignore this one */
    } while ( (res = res->ai_next) != NULL);

    if (res == NULL)            /* errno set from final connect() */
        err_sys ("tcp_connect error for %s, %s", host, serv);
    freeaddrinfo (ressave);
    return (sockfd);
}

listen:
listen函数仅由TCP服务器调用,主要实现两个功能:
  • 当socket函数创建一个套接字时,它被设为一个主动套接字(一个将调用connect发起连接的客户套接字)。listen函数把一个未连接的套接字转换成一个被动套接字(监听套接字),指示内核应接受指向该套接字的连接请求。根据TCP状态转换图,调用listen函数导致套接字从CLOSED状态转换到LISTEN状态。
  • listen函数规定了内核应该为相应套接字排队的最大连接个数。
#include 
int listen(int sockfd, int backlog);
函数参数:
  • sockfd监听连接的套接字
  • backlog指定了正在等待连接的最大队列长度,它的作用在于处理可能同时出现的几个连接请求,DoS(拒绝服务)攻击即利用这个原理,非法的连接占用了全部的连接数,造成正常的连接请求被拒绝
返回值:成功   0,出错   -1
listen(listenfd,5);

内核为任何一个给定的监听套接字维护两个队列:
未完成连接队列(incomplete connection queue)
每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
已完成连接队列(completed connection queue)
每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

传输层TCP协议——TCP套接字编程_第1张图片
每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是自动的,无需服务器进程插手。
传输层TCP协议——TCP套接字编程_第2张图片
  • 当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应三路握手的第二个分节:服务器的SYN响应,其中捎带对客户SYN的ACK。创建的新项一直保留在未完成连接队列,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列的队头项将返回给进程,如果已完成连接队列为空,进程将被投入睡眠,直到TCP在已完成连接队列中放入一项才唤醒进程。
  • backlog * 1.5 = 未完成连接队列的最大长度,通常指定为5的backlog值实际允许最多有8项排队。
  • 在三路握手正常完成的前提下(没有丢失分节,没有重传),未完成连接队列中的任何一项在其中的存留时间就是一个RTT
tcp_ listen函数:
执行TCP服务器的通常步骤:创建一个TCP套接字,给它捆绑服务器的众所周知端口,并允许接受外来的连接请求。
int tcp_listen(const char *hostname, const char *service, socklen_t *addrlenp);
返回值:成功则返回已连接套接字描述符,出错不返回
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
    int listenfd, n;
    const int on = 1;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof (struct addrinfo)) ;
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((n = getaddrinfo (host, serv, &hints, &res)) != 0)
        err_quit("tcp_listen error for %s, %s: %s",host, serv, gai_strerror(n)) ;
    ressave = res;
    do {
        listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (listenfd < 0)
            continue;            /* error, try next one */
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on) ) ;
        if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
        break;                   /* success */
        close (listenfd);        /* bind error, close and try next one */
    } while ( (res = res->ai_next) != NULL);

    if (res == NULL)             /* errno from final socket () or bind () */
        err_sys ("tcp_listen error for %s, %s", host, serv);
    listen (listenfd, LISTENQ);
    if (addrlenp)
        *addrlenp = res->ai_addrlen;     /* return size of protocol address */
    freeaddrinfo (ressave);
    return (listenfd);
}

accept:
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,进程被投入睡眠。
#include 
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
函数参数:
  • sockfd            接收客户连接的socket,即listening socket
  • addr                 返回已连接的对端进程的协议地址
  • *addrlen所引用的整数值                调用前,由cliaddr所指的套接字地址结构的长度;返回时,由内核存放在该套接字地址结构内的确切字节数
返回值:成功 已连接套接字(connected socket,由内核自动生成的一个全新的描述符),出错  -1

accept函数最多返回一下三个值中的其中之一:

  •  一个新的已连接套接字描述符
  • 出错指示的整数
  • 客户进程的协议地址(*cliaddr),该地址的大小(*addrlen

如果对返回客户协议地址无兴趣,将cliaddr,addrlen均置为空指针。

typedef struct sockaddr SA;
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd,(SA *)&cliaddr, &clilen);

recv/send:

ssize_t recv(int socket, const void *buffer, size_t length, int flags);
函数参数:

  • sockfd             socket返回的文件描述符
  • buffer               接收数据缓冲区的首地址
  • length               接收的字节数
  • flags                 接收方式 通常为0
返回值:成功   实际接收的字节数,出错   -1

int status = recv(sockfd, buf, sizeof(buf), 0);

ssize_t send(int socket, const void *buffer, size_t length, int flags);

函数参数:

  • sockfd             socket返回的文件描述符
  • buffer               发送数据缓冲区的首地址
  • length               发送的字节数
  • flags                 发送方式 通常为0
返回值:成功   实际发送的字节数,出错   -1

int status = send(sockfd, buf, sizeof(buf), 0);

read/write:

ssize_t read(int fd, const void *buf, size_t count);

读一个字节流套接字,从一个描述符读n字节

ssize_t readn(int fd, const void *buf, size_t nbytes);
ssize_t readn(int fd, void *vptr, size_t n)  /* Read "n" bytes from a descriptor. */  
{  
    size_t  nleft;  
    ssize_t nread;  
    char    *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0)
    {  
        if ((nread = read(fd, ptr, nleft)) < 0)
        {  
            if (errno == EINTR)  
                nread = 0;      /* and call read() again */  
            else  
                return(-1);  
        } else if (nread == 0)  
            break;              /* EOF */  
        nleft -= nread;  
        ptr += nread;  
    }  
    return(n - nleft);      /* return >= 0 */  
}  

ssize_t readline(int fd, void *buf, size_t maxlen);
static int  read_cnt; 
static char *read_ptr;  
static char read_buf[MAXLINE];  

static ssize_t my_read(int fd, char *ptr)	//每次最多读取MAXLINE个字符,调用一次,每次只返回一个字符  
{
    if (read_cnt <= 0) {  
        again:  
            if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) //如果读取成功,返回read_cnt=读取的字符
            {
                if (errno == EINTR)  
                    goto again;  
                return(-1);  
            } else if (read_cnt == 0)  
                return(0);  
        read_ptr = read_buf;  
    }  
    read_cnt--;	//每次递减1,直到<0读完,才执行上面if的命令。  
    *ptr = *read_ptr++;	//每次读取一个字符,转移一个字符  
    return(1);  
} 
  
ssize_t readline(int fd, void *vptr, size_t maxlen)  
{  
    ssize_t n, rc;  
    char c, *ptr;  
  
    ptr = vptr;  
    for (n = 1; n < maxlen; n++) {  
        if((rc = my_read(fd, &c)) == 1) {  
            *ptr++ = c;  
        if (c == '\n')  
            break;  /* newline is stored, like fgets() */  
        } else if (rc == 0) {  
            *ptr = 0;  
        return(n - 1);  /* EOF, n - 1 bytes were read */  
    } else  
        return(-1);     /* error, errno set by read() */  
    } 
  
    *ptr = 0;   /* null terminate like fgets() */  
    return(n);  
}  
// readlinebuf函数能够展露内部缓冲区的状态,便于调用者查看在当前文本行之后是否收到了新的数据
ssize_t readlinebuf(void **vptrptr)  
{
    if (read_cnt)  
        *vptrptr = read_ptr;  
    return(read_cnt);  
}

ssize_t write(int fd, const void *buf, size_t count);
写一个字节流套接字,往一个描述符写n字节
ssize_t written(int fd, const void *buf, size_t nbytes);
ssize_t writen(int fd, const void *vptr, size_t n)  /* Write "n" bytes to a descriptor. */  
{  
    size_t      nleft;  
    ssize_t     nwritten;  
    const char  *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0) {  
        if ((nwritten = write(fd, ptr, nleft)) <= 0) {  
            if (nwritten < 0 && errno == EINTR)  
                nwritten = 0;       /* and call write() again */  
            else  
                return(-1);         /* error */  
        }  
        nleft -= nwritten;  
        ptr += nwritten;  
    }  
    return(n);  
}

close/shutdown:

close函数用来关闭套接字,并终止TCP连接

int close(int socketfd);
  • 关闭双向通信。close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,TCP尝试发送已排队等待发送到对端的任何数据,完毕后发送TCP连接终止序列。
  • 在并发服务器中,fork一个子进程会复制父进程在fork之前创建的所有描述符,复制完成后,相应描述符的引用计数会增加1。父进程调用close关闭已连接套接字只会使相应描述符的引用计数减1,一旦描述符的引用计数为0,内核就会关闭该套接字。调用close后套接字的描述符引用计数仍然大于0的话,就不会引发TCP的四分组连接终止序列。

int shutdown(int sockfd, int howto);
使用shutdown可以不管引用计数就激发TCP的正常连接终止序列。
TCP连接是双向的(是可读写的),当我们使用close时 ,会把读写通道都关闭,有时我们希望只关闭一个方向,这时候我们可以使用shutdown。

针对不同的howto,系统会采取不同的关闭方式

  • SHUT_RD howto = 0
关闭读通道。套接字中不再有数据可接受,而且套接字接收缓冲区中的现有数据被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP这样调用shutdown函数后,该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。
  • SHUT_WR howto = 1
关闭写通道。当前留在套接字发送缓冲区的数据将被发送,后跟TCP的正常连接终止序列。不管套接字描述符的引用计数是否为0,写半部关闭照样执行,进程不能再对这样的套接字调用任何写函数。
  • howto = 2
关闭读写通道,同close()。与调用shutdown两次等效:第一次指定SHUT_RD,第二次指定SHUT_WR。




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