• 四、初识Socket套接字API


    初始Socket套接字API

    一、什么是Socket套接字

    1、socket是一种操作系统提供的进程间通信机制。


    2、操作系统中,通常会为应用程序提供一组应用程序接口(API),称为套接字接口(socket API)。应用程序可以通过套接字接口,来使用网络套接字,以进行数据交换。


    3、Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。而接口的具体实现都是内核来完成。


    客户与服务器进程的作用是非对称的,因此代码不同。

    服务器进程一般是先启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。

    二、TCP套接字编程API

    • TCP交互流程

    服务器

    1. 创建socket;
    2. 绑定socket和端口号;
    3. 监听端口号;
    4. 接收来自客户端的连接请求;
    5. 从socket中读取字符;
    6. 关闭socket。

    客户端

    1. 创建socket;
    2. 连接指定计算机的端口;
    3. 向socket中写入信息;
    4. 关闭socket。

    1、创建套接字

    int  socket(int domain, int type, int protocol);//返回sockfd
    

    domain 用于指定创建套接字所使用的协议族 协议族决定了socket的地址类型,在通信中必须采用对应的地址

    domain参数 参数含义
    AF_INET IPv4协议
    AF_INET6 Ipv6协议
    AF_LOCAL Unix协议域/只在本机内通信的套接字
    AF_ROUTE 路由套接字
    AF_KEY 秘钥套接字
    type参数 参数含义
    SOCK_STREAM 字节流套接字
    SOCK_DGRAM 数据报套接字
    SOCK_SEQPACKET 有序分组套接字
    SOCK_RAW 原始套接字

    protocol:故名思意,就是指定协议。通常设为0,通过参数domain指定的协议族和参数type指定套接字类型来确定参数。

    //创建TCP套接字
        //AF_INET:网络连接,ipv4
        //SOCK_STREAM:TCP连接
        int fd = socket(AF_INET, SOCK_STREAM, 0);
        if (fd<0) {
            std::cout<<"create socket error!"<<std::endl;
            return 0;
        }
        std::cout<<"create socket: "<<fd<<std::endl;
    

    2、绑定套接字

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);// 返回:成功为0,出错-1
    
    • sockfd:socket描述字,它是通过socket()函数的唯一标识的socket。

    • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。

    • addrlen:对应的是地址的长度。

    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

    bind函数绑定特定的IP地址必须属于其所在主机的网络接口之一,服务器在启动时绑定它们众所周知的端口,如果一个TCP客户端或服务端未曾调用bind绑定一个端口,当调用connect或listen时,内核就要为响应的套接字选择一个临时端口。让内核选择临时端口对于TCP客户端来说是正常的额,然后对于TCP服务端来说确实罕见的,因为服务端通过他们众所周知的端口被大家认识的。

    //命名套接字
        struct sockaddr_in myaddr;
        memset((void *)&myaddr, 0, sizeof(myaddr));
        myaddr.sin_family = AF_INET;
        myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        myaddr.sin_port = htons(6666);
        if (bind(fd, (struct sockaddr*)&myaddr, sizeof(myaddr)) < 0) {
            std::cout<<"name socket error!"<<std::endl;
            return 0;
        }
        std::cout<<"name socket"<<std::endl;
    

    3、建立连接

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);// 返回:成功为0,出错-1
    
    • sockfd:socket描述字,它是通过socket()函数的唯一标识的socket。

    • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。

    • addrlen:对应的是地址的长度。

    如果connect失败后,就必须close当前的套接字描述符并重新调用socket。客户端在调用connect前不必非得调用bind函数(比如UDP客户端编程中一般就不用调用bind),内核会确定源IP地址,并选择一个临时端口作为源端口。

    如果是TCP套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或出错时才返回。注意:connect是在接收到服务端响应的SYN+ACK时的返回的,也就是三次握手的第二次动作之后。

    UDP是可以调用connect函数的,但是UDP的connect函数和TCP的connect函数调用确是大相径庭的,这里没有三次握手过程。内核只是检查是否存在立即可知的错误(比如目的地址不可达),记录对端的IP和端口号,然后立即返回调用进程。使用了connect的UDP编程就可不必使用sendto函数了,直接使用write/read即可。

    4、接受连接

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //  返回:成功返回已连接描述符(非负),出错-1
    
    • sockfd:socket描述字,它是通过socket()函数的唯一标识的socket。

    • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。

    • addrlen:对应的是地址的长度。

    //等待并接受连接
        const int MAXBUF = 4096;
        char buff[MAXBUF];
        struct sockaddr_in client_addr;
        int client_addr_len = sizeof(client_addr);
        int client_fd;
        while (1) {
            client_fd = accept(fd, (struct sockaddr*)&client_addr, &client_addr_len);
            if (client_fd < 0) {
                std::cout<<"connect error"<<std::endl;
                continue;
            }
            //接收数据
            //关闭套接字
        }
    

    1、accept函数会返回一个新的socket描述子,这个新的描述子代表了服务端和客户端的连接。后面可以用于读取数据以及关闭连接。

    2、accept函数有TCP服务器调用,用于从已完成队列中列头返回下一个已完成连接,如果已完成队列为空,则进程被投入睡眠(如果该套接字为阻塞方式的话)。如果accept成功,那么其返回值是由内核自动生成的一个全新套接字,代表与返回客户的TCP连接,函数的第一个参数为监听套接字,返回值为已连接套接字。

    3、accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字

    5、端口监听

    int listen(int sockfd, int backlog);// 返回:成功返回0,出错-1
    
    • sockfd:socket描述字,它是通过socket()函数的唯一标识的socket。
    • backlog:队列的大小

    1、作为一个服务器,在调用socket()和bind()之后就会调用listen()来监听这个socket。为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求。

    调用listen函数将导致套接字从CLOSEE状态转换到LISTEN状态。第二个参数规定了内核应为相应套接字排队的最大连接个数。

    具体listen细节实现请参考:https://www.cnblogs.com/luoxn28/p/5819798.html

    //创建监听队列
    if (listen(fd, 5) < 0) {
        std::cout<<"listen failed"<<std::endl;
        return 0;
    }
    

    6、关闭套接字

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

    close一个TCP套接字的默认行为是把该套接字标记为已关闭,然后立即返回到调用进程。注意,close实质把该套接字引用值减1,如果该引用值大于0,则对应的套接字不会被真正关掉。

    关闭一个套接字描述符,该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数

    int shutdown(int s,int how);
    

    close类似,但是功能更强大,可以对套接字的关闭进行一些更细致的控制,它允许对套接字进行单向关闭或者全部禁止。

    SHUT_RD:读通道关闭,进程将不能再接收任何数据,接受缓冲区还未被读取的数据也将被丢弃,但仍然可以在该套接字上发送数据。

    SHUT_WR:写通道关闭,进程将不能再发送任何数据,发送缓冲区还未被发送的数据也将被丢弃,但仍然可以在该套接字上接收数据。

    SHUT_RDWR:读、写通道都被关闭

    7、TCP套接字的数据传输

    // 返回:成功为读入或写入的字节数,出错为-1
    ssize_t send(int s , const void *msg , size_t len , int flags);
    
    • 函数send只能对处于连接状态的套接字使用
      s:为已建立好连接的套接字描述符,即accept的返回值。
      msg:指向存放待发送数据的缓冲区,
      len:为待发送数据的长度。
      flags: 为控制选项,一般设为0或取以下值。

    1、发送的数据太长而不能发送时,将出现错误,errno为EMSGSIZE;如果要

    2、如果该套接字为阻塞方式,发送的数据长度大于该套接字的缓冲区剩余空间大小时,send一般会被阻塞,

    3、如果该套接字为非阻塞方式,发送的数据长度大于该套接字的缓冲区剩余空间大小时则此时立即返回-1并将errno设为EAGAIN

    4、执行成功返回实际发送数据的字节数,出错则返回-1
    注意 :执行成功只是说明数据写入套接字的缓冲区中,并不表示数据已经通过网络发送到目的地。

    ssize_t recv(int s , void *buf , size_t len , int flags);
    
    • 函数recv只能对处于连接状态的套接字使用
      s:为已建立好连接的套接字描述符,即accept的返回值。
      buf所指定的缓冲区,
      len则为缓冲区的长度。
      flags为控制选项,一般设置为0。

    如果一个数据包太长以至于缓冲区无法放下时,剩余部分的数据有可能被丢弃(根据套接字的类型)。

    如果该套接字为阻塞方式,在指定的套接字上无数据到达时,recv被阻塞,

    如果该套接字为非阻塞方式,在指定的套接字上无数据到达时,则立即返回-1并将errno设置为EAGAIN。recv接受到数据就返回,不会等到接受到参数len指定的长度才返回。

    执行成功返回实际接收数据的字节数,出错则返回-1


    三、UDP套接字编程API

    • UDP交互流程

    接收方

    1. 创建socket;
    2. 绑定socket和端口号;
    3. 监听端口号;
    4. 从socket中读取字符;
    5. 关闭socket。

    发送方

    1. 创建socket;
    2. 向socket中写入信息;
    3. 关闭socket。

    • UDP与TCP使用的是相同的API函数,这里不在重复叙述,唯一区别是相关的API参数不同,UDP不需要建立连接。

    1、UDP套接字的数据传输

    // 返回:成功为读入或写入的字节数,出错为-1
    ssize_t sendto(int s, const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
    
    

    sendto不需要套接字处于连接状态,常用来发送UDP数据。要指定数据目的地址和目的地址的长度。

    ssize_t recvfrom(int s, void *buf, size_t len,int flags,struct sockaddr *from, socklen_t *fromlen);
    

    用来接收UDP数据,需要指定源地址和源地址长度。

    recvfrom和snedto的前3个参数和read/write的前3个参数一样。flags表示设置的标志值,简单的UDP程序可以直接设置为0,最后两个参数表示服务端地址(对于sendto来说)或者是对端地址(对于recvfrom来说)。如果不关心对端的地址,则设置为NULL,此时addrlen也可以设置为NULL了。

    注意:recvfrom和sendto也可以应用于TCP编程,不过一般不这样用。UDP编程会有数据包的丢失问题,因为UDP是不可靠的,如果一个客户的数据包丢失,客户端将永远阻塞在recvfrom函数调用;类似的,如果客户数据到达了服务端,然后响应数据包丢失了,则客户永远阻塞在recvfrom调用。为了防止这样的问题出现,一般可以给recvfrom设置一个超时时间。
      

    四、字节序API

    参考:https://www.cnblogs.com/52php/p/6114080.html


    主机字节序:就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机字节序。

    a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

    b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

    网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
    所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。所以务必将其转化为网络字节序再赋给socket。

    点分10进制格式
    这是我们最常见的表示格式,比如某机的IP地址可能为“202.101.105.66”。事实上,对于Ipv4(IP版本)来说,IP地址是由一个32位的二进制数所构成,但这样一串数字序列无疑是十分冗长并且难以阅读和记忆的。为了方便人们的记忆和使用,就将这串数字序列分成4组,每组8位,并改为用10进制数进行表示,最后用小原点隔开,于是就演变成了“点分10进制表示格式”。
    来看看刚才那个IP地址的具体转化过程:

    IP地址:11001010011001010110100101000010
    分成4组后:11001010 01100101 01101001 01000010
    十进制表示:202 101 105 66
    点分表示:202.101.105.66
    
    • 源主机字节序整数转网络字节序
    uint32_t htonl(uint32_t hostlong);//将主机unsigned int型数据转换成网络字节顺序
    uint16_t htons(uint16_t hostshort);//将主机unsigned short型数据转换成网络字节顺序
    uint32_t ntohl(uint32_t netlong);//与htonl相反
    uint16_t ntohs(uint16_t netshort);//与htons相反
    
    • 源主机字符串转网络字节序
    //将cp所指向的字符串形式的IP地址转换为二进制的网络字节序的IP地址,执行成功返回非零值,参数无效返回
    int inet_aton(const char *cp, struct in_addr *inp);
    //inet_aton类似,执行成功将结果返回,参数无效返回INADDR_NONE,一般为-1,可能使“255.255.255.255”成为无效地址
    unsigned long inet_addr(const char FAR * cp)
    
    • 源主机字符转主机字节序
    //将字符串形式的网络地址转换为主机字节顺序形式的二进制IP地址,成功返回转换后的结果,参数无效返回-1
    in_addr_t inet_network(const char *cp);
    
    • 网络字节序转点分10进制
    //将值为in的网络字节顺序形式的二进制IP地址转换成以”.”分隔的字符串形式,执行成功返回结果字符串指针,参数无效返回NULL
    char *inet_ntoa(struct in_addr in);
    
    • 地址提取
    //从参数in提取出主机地址,执行成功返回主机字节顺序形式的主机地址
    in_addr_t inet_lnaof(struct in_addr in);
    
    //从参数in提取出网络地址,执行成功返回主机字节顺序形式的网络地址
    in_addr_t inet_netof(struct in_addr in);
    

    五、参考文献

    1、https://blog.csdn.net/ldx19980108/article/details/77072391
    2、https://www.cnblogs.com/wangzhao765/p/9161589.html
    3、https://www.cnblogs.com/luoxn28/p/5819798.html
    4、http://www.cnblogs.com/luoxn28/p/5811727.html
    5、https://blog.csdn.net/jiangyiaxiu/article/details/6826615 //解除端口绑定

  • 相关阅读:
    2018年春季个人阅读计划
    软件需求我们需要做到什么
    开发日志03
    开发日志02
    开发日志01
    软件需求模式阅读笔记2
    2020/2/11-Python学习计划
    2020/2/10-Python学习计划
    2020/2/9-Python学习计划
    2020/2/8-Python学习计划
  • 原文地址:https://www.cnblogs.com/retry/p/9338691.html
Copyright © 2020-2023  润新知