Socket编程

作者: 编程半岛 | 来源:发表于2018-08-05 20:28 被阅读8次

    1. 什么是socket?

    • socket可以看成是用户进程内核网络协议栈的编程接口;
    • socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信
    • 每一个套接口有一个地址属性

    2. IPv4套接口

    • IPv4套接口地址结构通常也称为网际套接字地址结构,它以sockaddr_in命名,定义在头文件<netinet/in.h>中:
    struct sockaddr_in {
        uint8_t sin_len;
      sa_family_t sin_family;
      in_port_t sin_port; /* 端口*/
      struct in_addr sin_addr; /* Internet 地址 */
      char sin_zero[8]; 
    };
    
    • sin_len:整个sockaddr_in结构体的长度,有些没有这个;
    • sin_family:指定该地址家族,在这里必须设为AF_INETAF_INET6表示IPv6地址族)
    • sin_port端口,无符号16位整数,最大值为65535
    • sin_addrIPv4的地址,无符号32位整数
    • sin_zero:暂不使用,一般将其设置为0
      通过man帮助手册参看地址结构:man 7 ip

    3. 通用的地址结构

    • 通用地址结构用来指定与套接字关联的地址:
    struct sockaddr {
        uint8_t sin_len;
      sa_family_t  sa_family; /* 地址家族, AF_xxx */
      char sa_data[14]; /*14字节协议地址*/
    };
    
    • sin_len:整个sockaddr_in结构体的长度,有些没有这个;
    • sin_family:指定该地址家族
    • sa_data:由sin_family决定它的形式

    4. 网络字节序

    • 大端字节序(Big Endian):最高有效位存储于最低内存地址处,最低有效位存储于最高内存地址处;
    • 小端字节序(Little Endian):最高有效位存储于最高内存地址处,最低有效位存储于最低内存地址处;
    • 主机字节序:不同的主机有不同的字节序;
    • 网络字节序:网络字节序规定为大端字节序;
      socket编程支持异构系统,不同的操作系统的字节序可能不相同,因此传输过程中统一成网络字节序

    测试本机操作系统是大端还是小端:

    #include <stdio.h>
    
    using namespace std;
    
    int main()
    {
        unsigned int a = 0x12345678;
    
        unsigned char* p = (unsigned char*)&a;
    
        printf("%0x, %0x, %0x, %0x\n", p[0], p[1], p[2], p[3]);
    
        return 0;
    }
    

    5. 字节序转换函数

    uint16_t htons(uint16_t hostshort)--"Host to Network Short"
    uint32_t htonl(uint32_t hostlong)--"Host to Network Long"
    uint16_t ntohs(uint16_t netshort)--"Network to Host Short"
    uint32_t ntohl(uint32_t netlong)--"Network to Host Long"

    6. 地址转换函数

    点分十进制地址与32为整数地址转换
    头文件:
    #include <netinet/in.h>
    #include <arpa/inet.h>

    int inet_aton(const char* cp, struct in_add* inp);
    in_addr_t inet_addr(const char* cp); // 将点分十进制地址转换为32位整数
    char* inet_ntoa(struct in_addr in); // 将地址结构(32位整数)转换为点分十进制

    #include <stdio.h>
    #include <arpa/inet.h>
    
    int main()
    {
        unsigned long addr = inet_addr("192.168.0.100");    // 将点分十进制地址转换为32位整数 
        printf("addr = %u\n", ntohl(addr));                 // 将32位整数转化为网络字节序
    
        
        struct in_addr ipaddr;
        ipaddr.s_addr = addr;
        printf("%s\n", inet_ntoa(ipaddr));                  // 将地址结构(32位整数)转换为点分十进制
        
        return 0;
    }
    

    7. 套接字类型

    • 流式套接字SOCK_STREAM(TCP协议):提供面向连接的,可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收
    • 数据报式套接字SOCK_DGRAM(UDP协议):提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱
    • 原始套接字SOCK_RAW:将应用层的数据跨越传输层,直接封装成网路层的数据

    8. TCP客户/服务器模型(C/S模型)

    C/S模型

    9. 回射客户/服务器模型

    回射客户/服务器模型

    10. socket函数

    • 头文件:<sys/socket.h>
    • 功能:创建一个套接字用于通信
    • 原型:int socket(int domain, int type, int protocol);
      domain:指定通信协议族(protocol family)
      type:指定socket类型,流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW
      protocol:协议类型
    • 返回值:成功返回非负整数,它与文件描述符类似,我们将它称为套接字描述字,简称套接字。失败返回-1

    man帮助手册输入man socket即可获得帮助文档

        // 创建套接字
        int listenfd;
        if ( (listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP))  < 0 )  // PF_INET(AF_INET):IPv4 Internet protocols, SOCK_STREAM:流式套接字
        {
            ERR_EXIT("socket");
        }
    

    11. bind函数

    • 头文件:<sys/socket.h>
    • 功能:绑定一个本地地址到套接字
    • 原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • 参数:
      sockfd:socket函数返回的套接字
      addr:要绑定的地址
      addrlen:地址长度
    • 返回值:成功返回0, 失败返回-1

    man帮助手册输入man bind即可获得帮助文档

        // 地址的初始化
        struct sockaddr_in servaddr;    // IPv4的地址结构
        memset(&servaddr, 0, sizeof(servaddr)); // 初始化地址
        servaddr.sin_family = AF_INET;      // 地址族
        servaddr.sin_port = htons(5188);    // 指定端口号,并将端口号转化为2个字节的网络字节序
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 绑定主机任意地址
        //servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");        // 指定地址  
        //inet_aton("127.0.0.1", &servaddr.sin_addr);       // 指定地址
    
        // 将套接字与地址绑定
        if( bind(listenfd, (struct sockaddr*)(&servaddr), sizeof(servaddr)) < 0 )
        {
            ERR_EXIT("bind");
        }
    

    12. listen函数

    • 头文件:<sys/socket.h>
    • 功能:将套接字用于监听进入的连接
    • 原型:int listen(int sockfd, int backlog);
    • 参数:
      sockfd:socket函数返回的套接字
    • backlog:规定内核为此套接字排队的最大连接个数

    调用listen函数后套接字变为被动套接字

    • 被动套接字:接收连接(调用accept函数接收连接)
    • 主动套接字(默认):用来发起连接(调用connect3函数发起连接)
        // 监听
        if( listen(listenfd, SOMAXCONN) < 0 )
        {
            ERR_EXIT("listen");
        }
    
    • 一般来说,listen函数应该在调用socket函数和bind函数之后,调用accept函数之前调用
    • 对于给定的监听套接口,内核需要维护两个队列:1.已由客户发出并到达服务器,服务器正在等待完成相应的TCP三次握手过程 2. 已完成连接的队列

    13. accept函数

    • 头文件:<sys/socket.h>
    • 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞
    • 原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    • 参数:
      sockfd:服务器套接字
      addr:将返回对等方的套接字地址
      addrlen:返回对等方的套接字地址长度
    • 返回值:成功返回非负整数,失败返回-1
        // 接收
        struct sockaddr_in peeraddr;    // 对方的地址
        socklen_t peerlen = sizeof(peeraddr);
        int conn;       // 定义已连接套接字
        if( accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen) < 0 )
        {
            ERR_EXIT("accept");
        }
    

    14. connect函数

    • 头文件:<sys/socket.h>
    • 功能: 建立一个连接至addr所指定的套接字
    • 原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      sockfd:未连接的套接字
      addr:要连接的套接字地址
      addrlen:地址长度
    • 返回值:成功返回0, 失败返回-1
        // 连接 connect
        if( connect(sock, (struct sockaddr*)(&cliaddr), sizeof(cliaddr))  < 0 )
        {   
            ERR_EXIT("connect");
        }
    

    15. REUSEADDR

    • 服务器端尽可能使用REUSEADDR
    • 在绑定之前尽可能调用setsockopt来设置REUSEADDR套接字选项
    • 使用REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器
        // 设置地址重复利用 REUSEADDR
        int on = 1;
        if( (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))  < 0) )
        {
            ERR_EXIT("setsockopt");
        } 
    

    16. process-per-connection

    • 一个连接一个进程来处理并发
    • 通过多进程来使得服务器可以接收多个客户端的消息,从而达到并发效果。父进程用来监听,子进程用来处理通信
        // 通过多进程来使得服务器可以接收多个客户端的消息,从而达到并发效果
        // 父进程用来监听,子进程用来处理通信
        pid_t pid;
        while(true)
        {
            if( (conn = accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen)) < 0 )
            {
                ERR_EXIT("accept");
            }
    
            printf("ip=%s, port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); // 打印客服端发送过来的ip和port
    
            pid = fork();
            if( pid == -1 )
            {
                ERR_EXIT("fork");
            }
            else if( pid == 0 )     // 子进程来处理通信
            {
                close(listenfd);    // 子进程不需要监听,将监听套接口关闭
                do_servece(conn);   // 通信处理函数
                exit(EXIT_SUCCESS); // 当客户端关闭后,子进程销毁
            }
            else    // 父进程
            {
                close(conn);    // 父进程不需要通信,将通信套接口关闭
            }
        }
    
    // 通信处理函数
    void do_servece(int conn)
    {
        char recvbuf[1024];
        while(true)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if( ret == 0 )          // 返回值为0,表示客户端关闭
            {
                printf("Client close");
                break;              // 当客户端关闭,退出循环
            }
            else if( ret == -1)
            {
                ERR_EXIT("read");
            }
            fputs(recvbuf, stdout);
            write(conn, recvbuf, ret);
        }
    }
    

    17. 点对点的聊天程序实现

    • 即两端能够聊天通信
    • 实现原理:在服务器和客户端都创建两个进程,一个用来接收数据,一个用来发送数据
      源码

    18. 流协议与粘包问题

    • TCP为字节流传输,无边界

    19. 粘包产生的原因

    20. 粘包处理方案

    21. readn 和 writen

    22. 完善回射客户/服务器程序

    相关文章

      网友评论

        本文标题:Socket编程

        本文链接:https://www.haomeiwen.com/subject/reybvftx.html