• TCP套接字编程


    一.套接字(socket)函数

        图1给出了在一个TCP客户与服务器通信的流程。服务器首先启动,稍后某个客户启动,它试图连接到服务器。假设客户给服务器发送一个请求,服务器处理该请求,并且给客户发回一个相应。这个过程一直持续下去,知道客户关闭连接的客户端,从而给服务器发送一个EOF(文件结束)通知为止。服务器接着也关闭连接的服务器端,然手结束运行或者等待新的客户连接。

    图1 客户/服务器程序的套接字函数

     

    1.socket函数

    #include<sys/socket.h>
    int socket(int family,int type,int protocol);
    

        为了执行一个I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型。其中family参数指明协议族,见表1;type参数指明套接字类型,见表2;protocol参数指明协议,见表3。 

    family 说明
    AF_INET IPv4协议
    AF_INET6 IPv6协议
    AF_LOCAL Unix域协议
    AF_ROUTE 路由套接字
    AF_KEY 密钥套接字

    表1 socket函数family常值

    type 说明
    SOCK_STREAM 字节流套接字
    SOCK_DGRAM 数据报套接字
    SOCK_SEQPACKET 有序分组套接字
    SOCK_RAW 原始套接字

    表2 socket函数type常值

    protocol 说明
    IPPROTO_TCP TCP传输协议
    IPPROTO_UDP UDP传输协议
    IPPROTO_SCTP SCTP传输协议

    表3 socket函数AF_INET或AF_INET6的protocol常值

        socket函数在成功时返回一个套接字描述符(sockfd),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

     

    2.connect函数

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

        TCP客户用connect函数来与服务器建立连接。第一个参数是socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。下面给出IPv4和IPv6套接字的地址结构,帮助理解:

    //IPv4套接字地址结构
    struct in_addr {
        int_addr_t       s_addr;     /* 32-bit IPv4 address */
                                              /* network byte order */
    };
    
    struct sockaddr_in {
        uint8_t            sin_len;        /* len of structure */
        sa_family_t      sin_family;   /* AF_INET */
        in_port_t         sin_port;      /* 16-bit TCP or UDP port number */
                                                 /* network byte order */
        struct in_addr  sin_addr;     /* 32-bit IPv4 address */
                                                 /* network byte order */
        char                sin_zero[8]; /* unused */
    };
    
    //IPv6套接字地址结构 struct in6_addr { uint8_t s6_addr; /* 128-bit IPv6 address */ /* network byte order */ }; #define SIN6_LEN /* required for compile-time tests */ struct sockaddr_in6 { uint8_t sin6_len; /* len of structure */ sa_family_t sin6_family; /* AF_INET */ in_port_t sin6_port; /* 16-bit TCP or UDP port number */ /* network byte order */ uint32_t sin_flowinfo; /* flow information, undefined */ struct in6_addr sin6_addr; /* 32-bit IPv4 address */ /* network byte order */ uint32_t sin6_zero[8]; /* unused */ };

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

        调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或者出错时才返回。三次握手建立连接会在后面介绍。

     

    3.bind函数

    #include<sys/socket.h>
    int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrle);
    

        当我们调用socket函数时,返回的socket描述字它存在于协议族中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind函数,否则系统会自动随机分配一个端口。

    4.listen函数

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

        其中参数backlog规定了内核应该为相应套接字排队的最大连接数。

        listen函数仅由TCP服务器调用,当socket函数创建一个套接字时,它被假设为一个主动套接字。而listen函数的作用就是把一个未连接套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求,即将该套接字从CLOSED状态转换到LISTEN状态。

    5.accept函数

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

         accept函数由TCP服务器调用,用于从已完成连接队列对头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠。

        如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。在accept函数中,第一个参数是监听套接字描述符(由socket创建,然后用作bind和listen的第一个参数的描述符),它的返回值为已连接套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。

    6.close函数

    #include<sys/socket.h>
    int close(int sockfd);
    

       close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能在有调用进程时使用。

    二.传输控制协议(TCP)

    1.TCP协议

           TCP是面向连接的通信协议,通过三次握手建立连接,所以只能用于端到端的通信。TCP提供一种可靠的数据流服务,当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传并等待,数次失败后TCP才放弃。注意TCP并不保证数据一定会被对方端点接收,准确来说,它提供的是数据的可靠传递或故障的可靠通知。

    2.TCP三次握手建立连接

    • 建立一个TCP连接会发生下述情形:
    • 服务器必须准备好接收外来的连接,即被动打开通常通过调用socket、bind和listen这3个函数来完成。
    • 客户通过调用connect发起主动打开。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。
    • 服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
    • 客户必须确认服务器的SYN。
    • 这种交换至少需要3个分组,因此称为TCP的三次握手,如图2所示。

    图2 TCP三次握手建立连接

        图2中,客户的初始序列号为J,服务器的初始序列号为K。ACK中的确认号是发送这个ACK的一端所期待的下一个序列号。因为SYN占据一个字节的序列号空间,所以每一个SYN的ACK中的确认号就是该SYN的初始序列号加1。类似的,每一个FIN(表示结束)的ACK中的确认号为该FIN的序列号加1。

    3.TCP四次握手释放连接

    • TCP建立一个连接需要3个分节,终止一个连接需4个分节。
    • 某个应用进程首先调用close,即该端执行主动关闭,该端的TCP发送一个FIN分节,表示数据发送完毕。
    • 接收到这个FIN的对端执行被动关闭。这个FIN由TCP确认,它的接收也作为一个文件结束符(EOF)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用程序在相应连接上再无额外数据接收。
    • 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字,这导致它的TCP也发送一个FIN。
    • 接收这个最终的FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

    图3 TCP四次握手释放连接

        类似SYN,一个FIN也占据一个字节的序列号空间,因此每个FIN的ACK确认号就是这个FIN的序列号加1。

    4.TCP状态转换图

        TCP连接建立和种植的操作可用状态状态转换图表示,如图4所示。

     图4 TCP状态转换图

        图中给出了11种TCP状态的名称,这些状态可使用netstat监视。

     

    三.一个简单的客户/服务器程序

        写了一个简单的TCP客户/服务器聊天程序作为全文总结:http://www.cnblogs.com/Rosanna/p/3494280.html

    服务器代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    #include<stdio.h>
    #include<stdlib.h>
    #include<errno.h>
    #include<string.h>
    #include<sys/types.h>
    #include<arpa/inet.h>
    #include<netinet/in.h>
    #include<sys/socket.h>
    #include<sys/wait.h>
    #include <unistd.h>
     
    #define MAXSIZE 1024
    #define PORT 8080
    #define BACKLOG 10
     
    int main(int argc,char **argv)
    {
        int listenfd,connfd;
        struct sockaddr_in servaddr,cliaddr;
        socklen_t len;
        char message[MAXSIZE];
     
        if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
        {
            perror("socket");
            exit(1);
        }
        else printf("socket create success! ");
     
        bzero(&servaddr,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
        servaddr.sin_port=htons(PORT);
     
        if((bind(listenfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr)))==-1)
        {
            perror("bind");
            exit(1);
        }
        else printf("bind success! ");
     
        if(listen(listenfd,BACKLOG)==-1)
        {
            perror("listen");
            exit(1);
        }
        else printf("sever is listening! ");
     
        for( ; ; )
        {
            printf("*********开始聊天********* ");
            len=sizeof(struct sockaddr);
            if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))==-1)
            {
                perror("accept");
                exit(1);
            }
            else printf("客户端:%s: %d ",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
            for( ; ; )
            {
                bzero(message,MAXSIZE);
                printf("输入:");
                fgets(message,MAXSIZE,stdin);
                if(!strncasecmp(message, "quit", 4))
                {
                    printf("终止聊天! ");
                    break;
                }
                else len=send(connfd,message,strlen(message),0);
                if(len<0)
                {
                    printf("发送失败");
                    break;
                }
     
                bzero(message,MAXSIZE);
                len=recv(connfd,message,MAXSIZE,0);
                if(len>0) printf("客户端:%s",message);
                else if(len<0) printf("接受消息失败! ");
                else printf("客户端不在线! ");
            }
            close(connfd);
            printf("是否退出服务器[Y/N]:");
            bzero(message,MAXSIZE);
            fgets(message,MAXSIZE,stdin);
            if(!strncasecmp(message, "Y", 1))
            {
                printf("服务器已退出!");
                break;
            }
        }
        close(listenfd);
        return 0;
    }

     客户端代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <errno.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <sys/types.h>
    #include <unistd.h>
     
    #define MAXSIZE 1024
    #define PORT 8080
     
    int main(int argc, char **argv)
    {
        int sockfd;
        struct sockaddr_in servaddr;
        socklen_t len;
     
        char message[MAXSIZE];   
         
        if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
        {
            perror("socket");
            exit(1);
        }
        else printf("socket create success! ");
     
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(PORT);
     
        inet_aton(argv[1],&servaddr.sin_addr);
     
        if(connect(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr))==-1)
        {
            perror("connect");
            exit(1);
        }
        else printf("conncet success! ");
         
        for( ; ; )
        {
            bzero(message,MAXSIZE);
            len=recv(sockfd,message,MAXSIZE,0);
            if(len>0)
                printf("服务器:%s",message);
            else
            {
                if(len<0) printf("接受消息失败! ");
                else printf("服务器已退出! ");
                break;   
            }
     
            bzero(message,MAXSIZE);
            printf("输入:");
            fgets(message,MAXSIZE,stdin);
     
            if(!strncasecmp(message, "quit", 4))
            {
                printf("client 请求终止聊天! ");
                break;
            }  
            else
                len = send(sockfd,message,strlen(message),0);
            if(len<0)           
            {
                printf("消息发送失败! ");
                break;           
            }
        }
        close(sockfd);
        return 0;
    }

     编译:

    1
    2
    gcc -Wall server.c -o server
    gcc -Wall client.c -o client

     服务器运行结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ./server
    socket create success!
    bind success!
    sever is listening!
    *********开始聊天*********
    客户端:127.0.0.1: 60355
    输入:hello
    客户端不在线!
    输入:quit
    终止聊天!
    是否退出服务器[Y/N]:Y
    服务器已退出!

    客户端运行结果:

    1
    2
    3
    4
    5
    6
    ./client 127.0.0.1
    socket create success!
    conncet success!
    服务器:hello
    输入:quit
    client 请求终止聊天!
  • 相关阅读:
    开放就像死亡访问之后就能回头——Leo鉴书84
    将博客搬至CSDN
    将博客搬至CSDN
    滚动条
    Perl Pack写的一个数据报表程序
    利用hash 数组打印标题
    Linux显示只显示目录文件
    Linux显示按文件名降序文件
    Linux显示以时间生升序显示文件
    Linux显示按文件大小降序排列
  • 原文地址:https://www.cnblogs.com/Rosanna/p/3494280.html
Copyright © 2020-2023  润新知