Socket编程基础

488 阅读11分钟

通用API

网络字节序转换

字节序分为大端字节序(big endian)和小端字节序(little endian) 。大端字节序是指一个整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7 bit)存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。当格式化的数据(比如32 bit整型数和16 bit短整型数)在两台使用不同字节序的主机之间直接传递时必出错。解决问题的方法是:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换) 。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。

即使是同一台机器上的两个进程(比如一个由C语言编写,另一个由JAVA编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。

Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:

#include<netinet/in.h>
unsigned long int htonl(unsigned long int hostlong); //主机字节->网络字节 长整型转换
unsigned short int htons(unsigned short int hostshort); //主机字节->网络字节 短整型转换
unsigned long int ntohl(unsigned long int netlong); //网络字节->主机字节
unsigned short int ntohs(unsigned short int netshort);

IP地址转换

下面3个函数可用于用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换

#include<arpa/inet.h>
in_addr_t inet_addr(const char*strptr);
int inet_aton(const char*cp,struct in_addr*inp);
char* inet_ntoa(struct in_addr in);
  • inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE。
  • inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1,失败则返回0。
  • inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。

下面这对更新的函数也能完成和前面3个函数同样的功能,并且它们同时适用于IPv4地址和IPv6地址

#include<arpa/inet.h>
int inet_pton(int af,const char*src,void*dst);
const char*inet_ntop(int af,const void*src,char*dst,socklen_t cnt);

Socket编程

最基本的socket通信流程大概如下

创建socket-socket()

UNIX/Linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
  • domain:告诉系统使用哪个底层协议族。对TCP/IP协议族而言,该参数设置为PF_INET(Protocol Family of Internet,IPv4)或PF_INET6(IPv6),也可以设置为AF_INET,表示地址家族,头文件中定义AF_INET = PF_INET ,两者值相同(协议族与地址族一一对应)
  • type:指定服务类型,主要有SOCK_STREAM服务(流服务)和SOCK_DGRAM(数据报)服务,TCP使用SOCK_STREAM ,UDP使用SOCK_DGRAM
  • protocol:在前两个参数构成的协议集合下,再选择一个具体的协议,这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,都应该把它设置为0,表示使用默认协议
listen_fd_ = socket(PF_INET, SOCK_STREAM, 0);

socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno,默认创建的socket是阻塞的,在后面的系统调用函数中socket默认会阻塞直到事件到来

绑定socket-bind()

创建socket时指定了地址族,但并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给socket命名**。在服务器程序中,通常要命名socket,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址**

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

bind()my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errnoEACCESEADDRINUSE

  • EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误
  • EADDRINUSE,被绑定的地址正在使用中。如将socket绑定到一个处于TIME_WAIT状态的socket地址
struct sockaddr_in addr{};
addr.sin_family = AF_INET; //ipv4
addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip转换,INADDR_ANY:0.0.0.0 任意ip,系统将绑定端口,由路由表决定ip
addr.sin_port = htons(port_);             // 端口转换
int ret = bind(listen_fd_, (struct sockaddr *) &addr, sizeof(addr));

sockaddr

socket网络编程接口中表示socket地址的是结构体**sockaddr****,socket绑定时需要传入该类型参数,现在基本不再使用,Linux为各个协议族提供了专门的socket地址结构体sockaddr_in**

#include<bits/socket.h>
struct sockaddr
{
    sa_family_t sa_family;
    char sa_data[14];
}

sockaddr_in

TCP/IP协议族有**sockaddr_insockaddr_in6两个专用socket地址结构体,分别用于IPv4和IPv6,socket地址就是指定协议的ip:port**

struct sockaddr_in
{
    sa_family_t sin_family; /*地址族:AF_INET*/
    u_int16_t sin_port; /*端口号,要用网络字节序表示*/
    struct in_addr sin_addr; /*IPv4地址结构体*/
};
struct in_addr
{
    u_int32_t s_addr;/*IPv4地址,要用网络字节序表示*/
};

设置socket地址:

struct sockaddr_in addr{};
addr.sin_family = AF_INET; //ipv4
addr.sin_port = htons(port_);             // 端口转换
addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip转换,INADDR_ANY:0.0.0.0 任意ip,系统将绑定端口,由路由表决定ip

INADDR_ANY 表示监听0.0.0.0地址,socket只绑定端口,不绑定本主机的某个特定ip,让路由表决定传到哪个ip(一台主机中如果有多个网卡就有多个ip地址)路由表应该能知道这个端口正在由哪个ip监听

所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型**sockaddr****(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr**

监听socket-listen()

socket被命名之后,还不能马上接受客户连接,需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接

#include<sys/socket.h>
int listen(int sockfd,int backlog);
  • sockfd指定被监听的socket
  • backlog内核监听队列的最大长度,如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息

listen成功时返回0,失败则返回-1并设置errno

int ret = listen(listenFd_, 6);

接受连接-accept()

#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
  • sockfd参数是执行过listen系统调用的监听socket
  • addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。accept失败时返回-1并设置errno。

accept只是从监听队列中取出连接,而不论连接处于何种状态

客户端发起连接-connect()

服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:

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

sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度。

connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:

  • ECONNREFUSED,目标端口不存在,连接被拒绝。
  • ETIMEDOUT,连接超时

所以发起连接时首先要知道目标服务器的ip与端口

读写数据recv()&send()

对文件的读写操作read()write()同样适用于socket。但socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:

#include<sys/types.h>
#include<sys/socket.h>

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);

recv()读取sockfd上的数据,buflen参数分别指定读缓冲区的位置和大小,flags参数通常设置为0即可。recv()成功时返回实际读取到的数据的长度,它可能小于期望的长度len。因此可能要多次调用recv()才能读取到完整的数据。

recv()返回0表示通信对方已经关闭连接了,需要对这种情况进行处理,否则服务器需要等待较长时间,导致出现读写异常。recv()出错时返回-1并设置errno

send()sockfd上写入数据,buflen参数分别指定写缓冲区的位置和大小。send()成功时返回实际写入的数据的长度,失败则返回-1并设置errno

关闭连接-close()

关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:

#include<unistd.h>
int close(int fd);

fd参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。

如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门为网络编程设计的):

#include<sys/socket.h>
int shutdown(int sockfd,int howto);

howto参数决定了shutdown的行为,shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。

shutdown成功时返回0,失败则返回-1并设置err

设置Socket选项

setsockopt

设置socket连接选项

int setsockopt(int sockfd,int level,int option_name,const void* option_value,socklen_t option_len);
  • sockfd:指定被操作的目标socket,传入socket文件描述符
  • level:指定要操作哪个协议的选项(即属性),如IPv4、IPv6、TCP等
  • option_name:则指定选项的名字。
  • option_value和option_len:分别是被操作选项的值和长度

文件描述符控制

fcntl()

fcntl函数( file control)描提供了对文件描述符的各种控制操作

#include<fcntl.h>
int fcntl(int fd,int cmd,...);

fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操作。根据操作类型的不同,该函数可能还需要第三个可选参数arg。

最常见的设置是给创建的socket增O_NONBLOCK标志来将socket设置为非阻塞模式

int old_option=fcntl(fd,F_GETFL,0);// 获取文件描述符旧的状态标志
int new_option=old_option|O_NONBLOCK; //设置非阻塞标志
fcntl(fd,F_SETFL,new_option);
// 简写
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);

实例

simple_server

/*
 * @Author: lsl
 * @Date: 2022-07-04 21:20:41
 * @Last Modified by: lsl
 * @Last Modified time: 2022-08-28 21:36:48
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <algorithm>
#include <vector>
#include <iostream>
//#include <bits/fcntl.h>
#include <fcntl.h>
#include "spdlog/spdlog.h"

const int BUF_SIZE = 1024;

void create_simple_socket_server(const int &port, const int &listen_backlog) {
    // create
    int listen_fd = socket(PF_INET, SOCK_STREAM, 0); // 创建socket,ipv4,字符流(tcp)
    if (listen_fd < 0) {
        spdlog::error("create socket error!");
        close(listen_fd);
        return;
    }
    // bind
    struct sockaddr_in address{};
    address.sin_family = AF_INET; // ipv4
    address.sin_port = htons(port);
    address.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    int ret = bind(listen_fd, (struct sockaddr *) &address, sizeof(address));
    if (ret < 0) {
        spdlog::error("bind socket error! port:{}", port);
        close(listen_fd);
        return;
    }
    // listen
    ret = listen(listen_fd, listen_backlog);
    if (ret < 0) {
        spdlog::error("listen port error!");
        close(listen_fd);
        return;
    }

    struct sockaddr_in client; // 客户端tcp连接描述,等待符合格式的连接
    socklen_t client_addr_len = sizeof(client);

    std::vector<int> client_fds;       // 监听多个客户端连接
    std::vector<int> close_client_fds; // 关闭的客户端连接
    char buffer[BUF_SIZE];             // 接收写数据

    // 设置socket非阻塞
    fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL, 0) | O_NONBLOCK);
    spdlog::info("select server running");
    // 处理事件,读写循环
    while (true) {
        int client_fd = accept(listen_fd, (struct sockaddr *) &client, &client_addr_len);
        if (client_fd > 0) {
            spdlog::info("accept socket:{}", client_fd);
            client_fds.push_back(client_fd);
        }
        // 遍历监听的文件描述符,判断是否有事件,阻塞直到事件到来
        for (auto client_fd: client_fds) {
            ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0); // 读取数据
            if (ret < 0) {
                spdlog::info("读取普通数据失败,关闭客户端连接,错误码:{}", ret);
                close(client_fd);
                close_client_fds.push_back(client_fd); // 主动关闭客户端连接,返回0,客户端主动断开连接,被动关闭
            } else if (ret == 0) {
                spdlog::info("客户端关闭连接,client_fd:{}", client_fd);
                close(client_fd);
                close_client_fds.push_back(client_fd); // 主动关闭客户端连接,返回0,客户端主动断开连接,被动关闭
            } else {
                spdlog::info("读取数据:{}", buffer);
                bzero(buffer, sizeof(buffer)); // 清空buffer
            }
        }
        // 删除断开的客户端
        for (auto client_fd: close_client_fds) {
            client_fds.erase(std::find(client_fds.begin(), client_fds.end(), client_fd));
        }
        close_client_fds.clear();
    }
}

int main(int argc, char *argv[]) {
    create_simple_socket_server(18888, 10);
    return 0;
}

simple_client

/*
 * @Author: lsl
 * @Date: 2022-08-29 21:20:14
 * @Last Modified by:   lsl
 * @Last Modified time: 2022-08-29 21:20:14
 */

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>

#include "spdlog/spdlog.h"

void create_simple_socket_client(const int &port) {
    spdlog::info("simple socket客户端");

    int server_fd = socket(PF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        spdlog::error("create socket error!");
        close(server_fd);
        return ;
    }

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(port);

    // 连接服务器
    if (connect(server_fd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
        spdlog::error("连接服务器失败:{}", server_fd);
    } else {
        spdlog::info("连接服务器成功,输入end结束");
        std::string info;
        while (info != "end") {
            std::cin >> info;
            send(server_fd, info.c_str(), info.size(), 0); // 发送数据
        }
    }
    close(server_fd);
}
int main(int argc, char *argv[]) {
    create_simple_socket_client(8888);
    return 0;
}