Linux 下的 Socket 编程详解 (一) 开始

简介

在 Linux 环境下,Socket 套接字是计算机操作系统中用来编写 TCP/IP 通信的接口。它是一种 facade 模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口谋面。在 TCP/IP 协议族里,Socket 的位置如下如所示:

OSI 模型和网际协议族中的映射

Socket 起源于 Unix,而 Unix 基本哲学就是“一切皆文件”,都可以用“open->write/read->close”模式来操作。Socket 就是该模式的一个实现,Socket 即是一种特殊的文件,一些 Socket 函数就是对其进行的操作(读/写IO、打、关闭)

  • 服务器端的套接字操作的流程为:socket -> bind -> listen -> accept -> write/read ->close

  • 客户端的套接字操作的流程为:socket -> connect -> write/read -> close

服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

这些接口的实现都是内核来完成,我们的用户程序通过系统调用,使用内核接口,完成套接字的创建。

参考文档

一个最简单的例子

服务器端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 12345


void Server(){

    int socket_fd, connect_fd;  // 套接字描述符
    struct sockaddr_in servaddr;
    char buff[4096];

    // 创建 socket
    if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        std::cout << "套接字创建失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    // 初始化
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP 地址设置成 INADDR_ANY, 让系统自动获取本机的 IP 地址
    servaddr.sin_port = htons(PORT); // 设置端口

    // 将本地地址 bind 到 socket 上
    if(bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        std::cout << "绑定套接字失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    // 开始 listen 客户端连接
    if(listen(socket_fd, 10) == -1){
        std::cout << "开启监听失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    std::cout << "Waiting ......" << std::endl;

    // 阻塞直到有客户端连接, accept
    if((connect_fd = accept(socket_fd, (struct sockaddr*) nullptr, nullptr)) == -1){
        std::cout << "接受连接失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }


    // 接受客户端发送来的数据
    int n = recv(connect_fd, buff, sizeof(buff), 0);
    buff[n] = '\0';
    std::cout << buff << std::endl;

    // 向客户端发回数据
    char sendBack[] = "I Received";
    send(connect_fd, sendBack, strlen(sendBack), 0);

    close(connect_fd);
    close(socket_fd);
}

客户端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 12345

void Client() {

    int socket_fd;
    char buff[] = "hello";
    struct sockaddr_in servaddr;


    if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        std::cout << "套接字创建失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 将点分十进制IP地址转换成整数

    // 创建连接
    if(connect(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
        std::cout << "连接创建失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }


    // 发送数据到服务器端
    if(send(socket_fd, buff, strlen(buff), 0) < 0){
        std::cout << "数据发送失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    // 接受服务器发来的数据
    char sendBack[2048];
    int n = recv(socket_fd, sendBack, sizeof(sendBack), 0);
    sendBack[n] = '\0';
    std::cout << sendBack << std::endl;

    close(socket_fd);

}

代码解释

创建套接字(socket)

首先,不管在客户端还是在服务器端,都需要调用 socket 函数,创建套接字。调用函数后,会返回一个整形的数字,这个数字被称为文件描述符(因为套接字也是一个特殊的文件)。在后面的套接字操作中,会使用到这个描述符。

对于文件描述符和文件指针,可以去看另外的文章详解

int socket(int protofamily, int type, int protocol) 函数接受三个参数。

  1. protofamily :协议族,常用的协议族有:AF_INET(IPv4)AF_INET6(IPv6)AF_LOCAL(Unix 域 Socket)。协议族决定了 套接字的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 + PORT 的组合
  2. type :指定套接字的类型
    • SOCK_STREAM
    • SOCK_DGRAM
    • SOCK_RAW
    • SOCK_PACKET
    • SOCK_SEQPACKET
  3. protocol :指定运输层的协议,包括IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC。分别对应 TCP,UDP,SCTP,TIPC

绑定 IP 地址(bind)

使用 bind 函数绑定 IP 地址,

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

  • sockfd :套接字描述符,通过 socket 创建,唯一标识一个 socket。bind 函数就是将给这个描述符绑定一个名字
  • addr :一个 const struct sockaddr* 指针,指向要绑定给 sockfd 的协议地址。其中传入这个参数的结构体如下:
// ipv4
struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

/* Internet address. */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};

// ipv6
struct sockaddr_in6 { 
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr { 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};
  • addrlen :对应地址的长度

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

监听(listen)和连接(connect)

绑定 ip 地址后,服务器端就开启接听,等待客户端的连接。客户端创建套接字后,就通过指定 ip 地址连接服务器。

int listen(int sockfd, int backlog)

  • sockfd :套接字描述符
  • backlog :可以排队的最大连接个数

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

与服务器端的 bind 函数参数一样

应答(accept)

当有客户端 connect 到服务器,这时 accept 函数就会有响应,接受客户端的请求,这样连接就建立好了。之后就可以开始网络 I/O 操作,类似于不同的文件的读写 I/O。accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。

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

  • sockfd :套接字描述符
  • addr :这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
  • addrlen :上面 addr 结构的大小,同样也可置为 NULL

读(read)写(write)

建立连接后,就可以开始网络 I/O 的读写。网络 I/O 操作有下面几组:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

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

recv 函数从套接字描述符中读取内容,读取成功时,返回实际所读的字符数,如果小于 0 表示出错,send 函数同理。

flag :默认为 0。其他取值如下:

  • MSG_DONTROUTE 绕过路由表查找,send 独占取值
  • MSG_DONTWAIT 仅本操作非阻塞,send 和 recv 均可设置
  • MSG_OOB 发送或接收带外数据,send 和 recv 均可设置
  • MSG_PEEK 窥看外来消息,recv 独占取值
  • MSG_WAITALL 等待所有数据,recv 独占取值

关闭(close)

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

int close(int fd)

你可能感兴趣的:(Linux 下的 Socket 编程详解 (一) 开始)