没啥用的主页
学习杂记
C++网络编程(一)
Dec 11 2022

socket

网络编程的流程不外乎就是绑定——监听——链接——收发消息这几个步骤,那么我们的服务端而言也差不多是这几个步骤,

首先是声明监听的socket,他的原型长下面这样

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

我们可以调用socket函数来声明一个socket,他的参数含义分别为:协议簇(domain),报文类型(type),传输协议(protocol);由于只凭借前两个参数我们就几乎可以明白socket的协议了,所以第三个参数一般为0,特殊的情况可以大家自己去百度,第一个参数决定了ip地址的类型,他可选的类型有AF_INET,PF_INET6,他们分别对应的ipv4和ipv6.当然对于unix本地协议族而言,他还有更多选项,但由于我们这里仅仅讲服务器使用,所以这些就不提了。由于PF_xxx和AF_xxx 具有相同的含义,所以上面的也可以写成PF_INET,AF_INET6.那他妈的为什么要写两个,真的烦人。这几个宏的定义都在头文件<sys/socket.h>中。第二个参数则决定的是数据的传输格式,主要有这两个选项:SOCK_STREAM(流,对应tcp),SOCK_UGRAM(数据报,对应udp).

因此这里我们利用下面的代码来生成一个监听用的socket.

int listenfd = socket(AF_INET,SOCK_STREAM,0);

你可能会好奇为什么我们返回的值是一个int变量,而不是像C#那样返回一个socket对象。这是因为在linux中一切皆文件的理念。对于我们的socket而言也不例外。因此在这里我们返回的值其实是一个索引。它对应了一个文件。也被称之为文件描述符。
每一个文件都对应着一个索引,这样我们在操作文件的时候就可以直接利用索引来对其操作了。文件描述符便是我们的程序为了管理这些已经被打开的文件所创建的索引。所有对文件的操作都是通过操作文件描述符来实现的。需要注意的是,我们新打开一个文件所返回的索引值不是0,而是3,也就是我们这里listenfd,那么第一个连接的socket所返回的应该是4,这一点比较重要。可以解决后面IO复用时候的小问题。

详细的知识可以点击这里去了解。

Bind

当我们创建了一个监听的socket时,他还不知道去监听哪个ip(有多个网卡的话会有多个ip地址),哪个端口。因此我们要在这里利用Bind函数指明所监听的ip地址与端口号。

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

老规矩我们先说一下函数的参数,第一个便是我们监听的socket的文件操作符,第二个参数是一个结构体,负责指明ip和端口。原型如下

    #include<sys/socket.h>
    struct sockaddr {  
        sa_family_t sin_family;//地址族
        char sa_data[14]; //14字节,包含套接字中的目标地址和端口              
   }; 

由于他是一个位操作,即存放了4字节的ip地址,2字节的端口,由于我们想获得ip和端口就得对sa_data进行位操作,显得比较繁琐,所以我们一般会用sockeraddr_in来代替他,

    #include<netinet/in.h>
    #include<arpa/inet.h>
    struct sockaddr_in{
        sa_family_t sin_family;
        uint16_t    sin_port;
        struct      sin_addr;
        char        sin_zero[8];
    }

由于是对sockaddr的补充,因此他们可以直接强行转化,所以在后面的使用中我们将其转化为一个sockaddr的指针类型。
我们这里直接就用实例来告知大家怎么使用

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    //转化端口号
    addr.sin_port = htons(9999);
    //意思是所有的ip地址都监听该端口
    addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(listenfd,(const sockaddr*)&addr,sizeof(addr)) == -1){
        std::cout << "Failed : bind" << std::endl;
        return 0;
    }

首先是声明了变量,再设置地址族我不理解为什么前面socket已经声明过了这里还要声明一次。然后设置端口号。这里端口号不能直接输入数字,而是需要用函数htons转化一下。
最后我们需要设置我们所监听的ip地址,输入常量INADDR_ANY 意思是监听所有的IP地址。
最后我们对listenfd进行绑定。

listen

这一步就简单了许多,如果说前面的bind是监听,那只有调用了listen以后,客户端才能真正的连接上我们的服务端。

int listen(int sockfd, int backlog);

第一个参数是我们的listenfd,第二个参数是对应的socket的可以排队的最大连接数。这里的最大连接数并不是我们socket的连接成功的数量,而是正在连接中的数量,即三次握手还没成功的数量。当成功监听了以后返回0,失败了则返回-1并设置errno。

    if(listen(listenfd,5)== -1){
        std::cout << "Failed : listen"<<std::endl;
        return 0;
    }

accept

但我们的客户端发起连接的时候我们怎么才能建立连接呢。答案便是调用accept函数。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

第一个参数sockfd为服务器的socket描述字,第二个参数addr为指向struct sockaddr*的指针,用于返回客户端的协议地址,第三个参数addrlen为协议地址的长度。其中比较特殊的参数是第二个参数,它用于存储客户端的临时信息。而第三个参数,只能说不知道干嘛用的这里也就贴一个连接给大家把,我也就bb了。当连接成功后,便会创建一个新的socket文件,并返回他的文件操作符,这由于是第四个文件,因此这里的操作符最小都为4.

send 与 recv

既然socket也是文件,那肯定就可以用read和write了,但是不够吊,所以我们还是用帮我封装好的 send 与 recv函数负责网络中的信息传输与接收

    ssize_t recv_(int sockfd, void *buf, size_t len, int flags);
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

cpp为什么能加这么多奇怪的类型。。。 首先是参数,这里的第一个参数不再是我们的监听socket的文件操作符,而是与客户端通信的操作符,即accept的返回值,第二个参数为任意类型的缓冲区,一般来说是一个char类型的数组。第三个参数len则是期望发送的数量和可接收数量。至于最后的第四个参数一般设置为0.其他的值可以自己百度。

那么他们的返回值是什么呢。是实际上的接收数和发送数。

Close

对于一个不再使用了的socket我们可以使用close函数来关闭它。或者说是让他的引用数减一,只有当一个文件操作符的引用变成0了以后才会真正的被关闭。
完整代码如下

    #include <sys/socket.h>
    #include <iostream>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <cstring>
    #include <unistd.h>
    #include <vector>
    int main(){
        std::cout << "Loading--------------"<< std::endl;

        int listenfd = socket(AF_INET,SOCK_STREAM,0);
        if(listenfd == -1){
            std::cout<<"Failed : listen" << std::endl;
            return 0;
        }

        //bind
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        //转化端口号
        addr.sin_port = htons(9999);
        //意思是所有的ip地址都监听该端口
        addr.sin_addr.s_addr = INADDR_ANY;
        if(bind(listenfd,(const sockaddr*)&addr,sizeof(addr)) == -1){
            std::cout << "Failed : bind" << std::endl;
            return 0;
        }
    
        //listen
        int max_fd = 5;
        if(listen(listenfd,5)== -1){
            std::cout << "Failed : listen"<<std::endl;
            return 0;
        }
        //accept
        int clientid;
        char clientIP[INET_ADDRSTRLEN] = "";
        sockaddr_in    clientAddr;
        socklen_t      clientAddrLen = sizeof(clientAddr);

        char buff[1024]
        memset(buff,0,sizeof(buff));//将buff初始化

        clientid = accept(listenfd,(sockaddr*)&clientAddr,&clientAddrLen);
        int num = recv(clientid,buff,sizeof(buff)-1,0);
        int num2 = send(clientid,buff,num,0);
        close(clientid);
        close(listenfd);
        return 0;
    }

IP地址转化函数

在我们收到了一个连接以后,我们会可能需要转化他的ip地址。但是传输过来的值阅读性比较差,我们可以利用以下几个函数来进行转化

    #include <arpa/inet.h>
    in_addr inet_addr(const char* strptr)
    int inet_aton(const char* cp,struct in_addr* inp);
    char* inet_ntoa(struct in_addr in);

inet_addr 函数将点分十进制的字符串变为用网络字节序列整数表示的地址

inet_aton 的功能与inet_addr相同,只不过是将转化结果存储到指针inp所指向的地质结构中

inet_ntoa则是将网络字节整数表示的ipv4转化为点分十进制的字符串。我们在连接的时候可以使用这个函数来增加可读性

总结

我们已经了解了最简单一个socket服务器的api。但仍然有许多的细节没有记录,比如关于TCP发送的缓冲区,socket连接关闭时候发送剩下资源的操作,以及获得客户端的socketIP端口主机名等信息的方法。等以后有时间了再来补充把。

后面的章节我们会了解一些关于IO复用,以及高性能服务器的编写的内容。