Linux C++ 网络 socket 基础

socket 概述

在一个典型的 C\S 场景中,应用程序使用 socket 进行通信的方式如下:

  • 各个应用程序创建一个 socket。socket 是一个允许通信的“设备”,两个应用程序都需要用到它
  • 服务器将自己的 socket 绑定到一个众所周知的地址上使得客户端能够定位到它的位置

通信 domain

UNIX domain

Internet domain


socket 存在于一个通信 domain 中,它确定:

  • 识别出一个socket 的方法(即 socket “地址” 的格式),UNIX domain socket 使用路径名, 而 Internet domain socket 使用IP地址和端口号
  • 通信范围(即在位于同一主机的应用程序间,还是位于使用一个网络连接起来的不同主机的应用程序之间)
  • 具体用什么数据结构来表示。UNIX domain 用:sockaddr_un;Internet domain 使用 sockaddr_in;

现代操作系统都支持以下 domain :

  • UNIX(AF_UNIX) domain 允许在同一主机上的应用程序进行通信
  • IPv4(AF_INET) domain 允许在使用因特网协议第 4 版(IPv4)网络连接起来的主机上的应用程序之间进行通信
  • IPv6(AF_INET6)domain 允许在使用因特网协议第 6 版(IPv6)网络连接起来的主机上的应用程序之间进行通信

IPv4 和 IPv6 可合成为 Internet domain

在 sockaddr_in 结构体中会出现 PF 和 AF, PF 表示 “协议族” (protocol family),AF 表示 “地址族” (address family)
但是 PF 现在基本用不到了。

img

socket 类型

每个 socket 实现至少提供两种 socket:流(SOCK_STREAM)和数据报(SOCK_DGRAM)

流 socket 提供了一个可靠的双向的字节通信信道。

  • 可靠的:表示发送端的数据可以完整无缺地到底接收端或收到一个传输失败的通知
  • 双向的:表示数据可以在两个 socket 的任意方向上传输
  • 字节的:表示与管道一样不存在消息边界
    (不存在消息边界指的是:接受方接受的数据块大小是随意的,没有确定的。)

数据报 socket 允许数据以被称为数据报的消息的形式进行交换。它具有以下特点:

  • 不可靠
  • 无连接
  • 存在消息边界

在 Internet domain(AF_INET 或 AF_INET6) 中,数据报 socket 使用 UDP 协议,而流 socket 使用 TCP 协议。一般来讲在称呼这两种 socket 时不会使用术语 “Internet domain 数据报 socket” 或 “Internet domain 流 socket” ,而是分别使用术语 “UDP socket” 和 “TCP socket” 。

通用 socket 地址结构:struct sockaddr

struct sockaddr{
    sa_family_t sa_family;          /* Address family (AF_* constant) */
    char        sa_data[14];        /* Socket address (size varies according to socket domain) */
};

UNIX domain socket 地址:struct sockaddr_un

struct sockaddr{
    sa_family_t sun_family;          /* always AF_UNIX */
    char        sun_path[108];        /* null-terminated socket pathname */
};

一般向 sun_path 字段写入内容时使用 snprintf() 或 strncpy() 以避免缓冲区溢出。

UNIX domain 中的流 socket

以下实现一个简单的通信 domain 为 UNIX domain 的迭代式服务器(服务器在处理下一个客户端之前一次只处理一个客户端)

cs.h 头文件:

#include <errno.h>
#include <unistd.h>
#include <sys/un.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>

#define SV_SOCK_PATH "/tmp/codroc"
#define BUFSZ 100
#define BACKLOG 5

int errExit(char *msg){
    printf("%s error!\n", msg);
    exit(0);
}

server.c:

#include "cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    ssize_t readnum;
    int sfd, cfd;
    struct sockaddr_un addr;

    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(-1 == sfd)
        errExit("socket");
    
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if(-1 == remove(SV_SOCK_PATH) && errno != ENOENT)
        errExit("remove");

    if(-1 == bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)))
        errExit("bind");
    if(-1 == listen(sfd, BACKLOG))
        errExit("listen");

    for(;;){
        cfd = accept(sfd, NULL,NULL);
        if(-1 == cfd)
            errExit("accept");

        while((readnum = read(cfd, buf, BUFSZ)) > 0)
            if(readnum != write(1, buf, readnum))
                errExit("write");

        if(-1 == readnum)
            errExit("read");

        if(-1 == close(cfd))
            errExit("close");
    }
    exit(0);
}

client.c:

#include "cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    struct sockaddr_un addr;
    int cfd;
    ssize_t readnum;

    cfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(cfd == -1)
        errExit("socket");
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);
    if(-1 == connect(cfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)))
        errExit("connect");
    while((readnum = read(0, buf, BUFSZ)) > 0)
        if(readnum != write(cfd, buf, readnum))
            errExit("write");
    if(-1 == readnum)
        errExit("read");
    exit(0);
}

在 shell 中输入以下命令来测试:

gcc -o unix_server server.c
gcc -o unix_client client.c

./unix_server > b &         #服务器程序放到后台执行,并将输出重定向到文件 b
ls -lF /tmp/codroc          #查看套接字文件

cat *.c > a                 #将所有 .c 文件的内容写入 a 文件
./unix_client < a           #将 a 文件中的内容作为 unix_client 的输入

diff a b                    #比较下 a b 文件的差异,若无差异 diff 不会返回任何东西

注意在服务器终止之后,/tmp/codroc 文件还是存在的!这就是为什么在 server.c 中的 bind 函数前要先执行 remove 函数,来确保 SV_SOCK_PATH 是不存在的!

UNIX domain 中的数据报 socket

之前有讲到数据报 socket 是不可靠的,这是有前提的,那就是在网络上传输的情况下,数据报 socket 是不可靠的。但是在 UNIX domain 下的数据报传输是在内核中发送的,所以说是可靠的!

以下实现一个在 UNIX domain 下使用数据报进行通信的简单 C\S 程序:

ud_cs.h:


#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <ctype.h>

#define BUFSZ 10
#define SV_SOCK_PATH "/tmp/codroc"

void errExit(char *msg){
    printf("%s error!\n", msg);
    exit(0);
}

ud_dgram_server.c:

#include "ud_cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    struct sockaddr_un saddr, caddr;
    ssize_t readnum;
    int sfd, cfd;

    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if(-1 == sfd)
        errExit("socket");
    
    memset(&saddr, 0, sizeof(struct sockaddr_un));
    saddr.sun_family = AF_UNIX;
    strncpy(saddr.sun_path, SV_SOCK_PATH, sizeof(saddr.sun_path) - 1);

    if(-1 == remove(SV_SOCK_PATH) && errno != ENOENT)
        errExit("remove");
    
    if(-1 == bind(sfd, (struct sockaddr *) &saddr, sizeof(struct sockaddr_un)))
        errExit("bind");
    for(;;){
        memset(buf, 0, sizeof(buf));
        socklen_t len = sizeof(struct sockaddr_un);
        readnum = recvfrom(sfd, buf, BUFSZ, 0, (struct sockaddr *) &caddr, &len);
        if(-1 == readnum)
            errExit("recvfrom");
        printf("Server received %ld bytes from \"%s\"\n", (long) readnum, caddr.sun_path);
        for(int i = 0;i < readnum;i++)
            buf[i] = toupper(buf[i]);
        if(readnum != sendto(sfd, buf, readnum, 0, (struct sockaddr *) &caddr, len))
                errExit("sendto");
    }
    exit(0);
}

ud_dgram_client.c:

#include "ud_cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    if(argc < 2 || 0==strcmp("--help", argv[1]))
        printf("%s msg...\n", argv[0]);
    struct sockaddr_un saddr, caddr;
    ssize_t readnum;
    int cfd;

    cfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if(-1 == cfd)
        errExit("socket");
    memset(&caddr, 0, sizeof(struct sockaddr_un));
    caddr.sun_family = AF_UNIX;
    snprintf(caddr.sun_path, sizeof(caddr.sun_path) - 1, "/tmp/codroc_client.%ld", (long)getpid());
    
    memset(&saddr, 0, sizeof(struct sockaddr_un));
    saddr.sun_family = AF_UNIX;
    strncpy(saddr.sun_path, SV_SOCK_PATH, sizeof(struct sockaddr_un) - 1);

    if(-1 == remove(caddr.sun_path) && errno != ENOENT)
        errExit("remove");
    if(-1 == bind(cfd, (struct sockaddr *) &caddr, sizeof(struct sockaddr_un)))
        errExit("bind");
    for(int i = 1;i < argc;i++){
        size_t len = strlen(argv[i]);
        memset(buf, 0, BUFSZ);
        socklen_t socklen = sizeof(struct sockaddr_un);
        if(len != sendto(cfd, argv[i], len, 0, (struct sockaddr *) &saddr, socklen))
            errExit("sendto");
        printf("Client sended %ld bytes: \"%s\" to Server \"%s\"\n", (long) len, argv[i], saddr.sun_path);
        readnum = recvfrom(cfd, buf, BUFSZ, 0, NULL, NULL);
        printf("Client received %ld bytes from \"%s\"\t", (long) readnum, saddr.sun_path);
        printf("the string is: \"%s\"\n", buf);
    }
    close(cfd);
    return 0;
}

在 shell 中输入以下命令来测试:

gcc -o ud_dgram_server ud_dgram_server.c
gcc -o ud_dgram_client ud_dgram_client.c

./ud_dgram_server  &         #服务器程序放到后台执行,并将输出重定向到文件 b
ls -lF /tmp/codroc          #查看套接字文件

./ud_dgram_client hello world
./ud_dgram_client helloworld codroc
./ud_dgram_client helloworldxxxx

在 执行 ./ud_dgram_client helloworldxxxx 可以看到服务器只能接受到10个字符,剩余的都被截掉了!

SOCKET: Internet Domain

  • Internet domain 流 socket 是建立在 TCP 之上的,它们提供了可靠的双向字节流通信信道
  • Internet domain 数据报 socket 是建立在 UDP 之上的。

网络字节序

众所周知,不同的硬件结构以不同的顺序来存储一个多字节整数的字节。一般顺序分为两种:大端小端,先存储最高有效位的称为大端机,存储顺序与整数的字节顺序一致的是小端机。

规定在网络中传输的字节顺序以大端为标准

因此当主机中的数据打包发出去时,包中的数据肯定是以大端序排列的了。

那么如何将主机序转换成网络序呢?C语言库给我们提供了相应的函数:

#include <arpa/inet.h>

uint16_t htons(uint16_t);
uint32_t htonl(uint32_t);
uint16_t ntohs(uint16_t);
uint32_t ntohl(uint32_t);

这些函数命名规范为(例如第一个):

host to net short (表示 16 位)

在主机字节序和网络字节序一致的情况下,这些函数会简单的原样返回传递给它们的参数。

Internet socket 地址

一个 IPv4 socket 地址会存储在 sockaddr_in 结构体中,该结构在 <netinet/in.h> 中定义,具体如下:

struct in_addr{
    in_addr_t s_addr;           /* unsigned 32 bits address */
};
struct sockaddr_in{
    sa_family_t     sin_family; /* address family: AF_INET */
    in_port_t       sin_port;   /* unsigned 16 bits port */
    struct in_addr  sin_addr;   /* IPv4 address */
    unsigned char   __pad[X];   /* pad to size of 'sockaddr'(16 bytes) */
};

我们知道在网络中传输的数据肯定是二进制数据。但是对于 ip 地址来说我们看到的是点分十进制,那么在网络编程时,我们就要把点分十进制转换成二进制,幸运的是,库函数已经有相应的函数了。

#include <arpa/inet.h>
int inet_pton(int domain, const char *src_str, void *addrptr);
    // Return 1 on successful, 0 if src_str is not in presentation format, or -1 on error
const char *inet_ntop(int domain, const void *addrptr, char *dst_str, size_t len);
    // Return pointer to des_str on success, or NULL on error

这些函数名中,p 表示 “展现” (presentation), n 表示 “网络”。展现形式是人类可读的形式。
addrptr 都是指向 sockaddr_in 或 sockaddr_in6 结构(根据 AF_INET 或 AF_INET6)。

独立于协议的主机和服务转换

getaddrinfo() 函数将主机和服务名转换成 ip 地址和端口号。getnameinfo() 是 getaddrinfo() 的逆函数

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

int getaddrinfo(const char *host, const char *service,
                const struct addrinfo *hints, struct addrinfo **result);

    //Return 0 on success, or nonzero on error

/* addrinfo 结构如下 */
struct addrinfo{
    int                 ai_flags;
    int                 ai_family;      // AF_INET or AF_INET6
    int                 ai_socktype;    // SOCK_STREAM or SOCK_DGRAM
    int                 ai_protocol;
    size_t              ai_addrlen;     // sizeof(sockaddr_in) or sizeof(sockaddr_in6)
    char               *ai_canonname;   // host name
    struct sockaddr     *ai_addr;       // pointer to sockaddr_in or sockaddr_in6
    struct addrinfo     *ai_next;       // pointer to next addrinfo
};

该函数会动态创建一个链表,链表中的元素是 addrinfo 结构体对象。返回的 *result 是指向链表头部的指针

介绍一下 addrinfo 结构体中各个变量的含义:

  • ai_flags: 仅用于 hints 参数
  • ai_family: 地址族类型,可以是 AF_INET、AF_INET6、AF_UNIX,表示该 socket 地址结构的类型
  • ai_socktype: addrinfo 的对象作为返回参数时,只能是 SOCK_STREAMSOCK_DGRAM,前者表示用于 TCP,后者用于 UDP
  • ai_protocol: 该字段会返回与地址族和 socket 类型匹配的协议值(ai_family、ai_socktype、ai_protocol 三个字段为调用 socket() 创建该地址上的 socket 时所需的参数提供了取值)
  • ai_addrlen: 该字段给出了 ai_addr 指向的 socket 地址结构的大小(字节数)
  • ai_canonname: 该字段仅由第一个 addrinfo 结构对象使用并且其前提是在 hints.ai_flags 中使用了 AI_CANONNAME 字段
  • ai_addr: 指向 socket 地址结构对象
  • ai_next: 指向像一个 addrinfo 对象的指针

hints 参数:

hints 参数为如何选择 getaddrinfo() 返回的 socket 地址结构指定了更多的标准。当用作 hints 参数时只能设置 addrinfo 结构的 ai_flags、ai_family、ai_socktype 以及 ai_protocol 字段,其他不能使用的字段必须置为 0 或 NULL

hints.ai_family 指定了返回的 socket 地址结构的域,其取值可以是 AF_INET 或 AF_INET6 或其他 AF_*(只要由相应的实现即可)。如果需要获取全部域类型就令 hints.ai_family = AF_UNSPEC

hints.ai_socktype 字段指定了使用返回的 socket 地址结构的 socket 类型。如果为 SOCK_DGRAM,那么查询将会在 UDP 服务上执行,如果是 SOCK_STREAM,那么查询会在 TCP 服务上进行。如果不限定某种特点服务,就令 hints.ai_socktype = 0

hints.ai_protocol 字段为返回的地址结构选择了 socket 协议。这个字段一般总是为 0,表示接受任何协议

hints.ai_flags 字段是一个位掩码,它会改变 getaddrinfo 的行为


因为 getaddrinfo 会动态创建一个链表,那么我们不再使用该链表时就应该主动地去释放它。
用 freeaddrinfo 函数来释放这个链表:

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

void freeaddrinfo(struct addrinfo *result);

C\S 示例(流 socket)

以下是服务器代码:

is_seqnum.h

#ifndef __SOCK_IS_SEQNUM_H_
#define __SOCK_IS_SEQNUM_H_

#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define PORT_NUM "10086"
#define INT_LEN 30


ssize_t readLine(int fd, char *buffer, size_t n);
void errExit(char *msg);

#endif // __SOCK_IS_SEQNUM_H_

readLine.c

#include <stdio.h>
#include <errno.h>
#include "is_seqnum.h"


void errExit(char *msg){
    printf("%s errno!\n",msg);
    exit(-1);
}
/*
 *  @parameter fd: file descriptor
 *  @parameter buffer: return argument 
 *  @parameter n: max size of buffer
 */
ssize_t readLine(int fd, char *buffer, size_t n){
    if(buffer == NULL || n <= 0){
        errno = EINVAL;
        exit(-1);
    }
    char *buf = buffer;
    size_t hasRead = 0;
    char ch;
    int readNum;

    for(;;){
        readNum = read(fd, &ch, 1);
        if(readNum == -1)
            errExit("read");
        if(0 == readNum){
            if(hasRead == 0)
                return 0;
            else//EOF
                break;
        }
        else{
            if(hasRead < n - 1){
                *buf++ = ch;
                hasRead++;
            }
            if('\n' == ch)
                break;
        }        
    }
    *buf = '\0';
    return hasRead;
}

is_seqnum_sv.c

#define _BSD_SOURCE
#include <netdb.h>
#include "is_seqnum.h"
#define BACKLOG 5

char ipbuf[100];
const char* myntop(struct sockaddr *addr, socklen_t len){
    if(len == sizeof(struct sockaddr_in)){
        return inet_ntop(AF_INET, (struct sockaddr *) addr, ipbuf, len);
    }
    else{
        return inet_ntop(AF_INET6, (struct sockaddr *)addr, ipbuf, len);
    }
}
int str2int(char *str){
    int res = 0;
    while(*str!='\0'){
        res = 10 * res + (*str - '0');
        str++;
    }
    return res;
}
int main(int argc, char *argv[]){
    int optval;
    size_t readnum;
    int cfd, lfd;
    struct addrinfo saddr, caddr, hints;
    struct addrinfo *rp, *result;
    struct sockaddr_storage claddr;
    socklen_t addrlen;
    unsigned int seqnum;
    char reqLenStr[INT_LEN];
    char seqNumStr[INT_LEN];
    char buf[100];
#define ADDRSTRLEN (NI_MAXHOST + NI_MAXSERV + 10)
    char addrStr[ADDRSTRLEN];
    char host[NI_MAXHOST];
    char service[NI_MAXSERV];
    int i = 0, reqLen;

    if(argc > 1 && strcmp(argv[1], "--help") == 0){
        fprintf(stdout, "%s [init-seq-num]\n", argv[0]);
        exit(0);
    }

    seqnum = (argc == 1) ? 0 : str2int(argv[1]);

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; // TCP service
    hints.ai_family = AF_UNSPEC; // allows IPv4 and IPv6
    hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV;
    hints.ai_addr = NULL;
    hints.ai_canonname = NULL;
    hints.ai_next = NULL;

    if(0 != getaddrinfo(NULL, PORT_NUM, &hints, &result))
        errExit("getaddrinfo");

    for(rp = result; rp != NULL; rp = rp->ai_next){
        memset(buf, 0, sizeof(buf));
        printf("%d. rp->ai_family: %s\t rp->ai_socktype: %s\t", i++, rp->ai_family == AF_INET ? "AF_INET"
                : "AF_INET6", rp->ai_socktype == SOCK_STREAM ? "SOCK_STREAM" : "SOCK_DGRAM");
        if((lfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
            continue;
        if(setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
            errExit("setsockopt");
        
        printf("ip: %s\t\n", myntop(rp->ai_addr, rp->ai_addrlen));
        if(0 == bind(lfd, rp->ai_addr, rp->ai_addrlen))
            break; //success
        close(lfd);
    }

    if(rp == NULL)
        errExit("Could not bind socket to any address\n");

    if(-1 == listen(lfd, BACKLOG))
        errExit("listen");
    freeaddrinfo(result);

    for(;;){
        addrlen = sizeof(struct sockaddr_storage);
        cfd = accept(lfd, (struct sockaddr *) &claddr, &addrlen);
        if(-1 == cfd)
            errExit("accept");
        if(getnameinfo((struct sockaddr *) &claddr, addrlen, 
                    host, NI_MAXHOST, service, NI_MAXSERV, 0)==0)
            snprintf(addrStr, ADDRSTRLEN, "(%s, %s)", host, service);
        else
            snprintf(addrStr, ADDRSTRLEN, "(?UNKNOWN?)");
        printf("Connection from %s\n", addrStr);

        if(readLine(cfd, reqLenStr, INT_LEN) <= 0){
            close(cfd);
            continue;
        }
        reqLen = atoi(reqLenStr);
        if(reqLen <= 0){
            close(cfd);
            continue;
        }
        snprintf(seqNumStr, INT_LEN, "%d\n", seqnum);
        if(strlen(seqNumStr) != write(cfd, &seqNumStr, strlen(seqNumStr)))
            fprintf(stderr, "Error on write");
        seqnum += reqLen;

        if(close(cfd) == -1)
            errExit("close");
    }
}

最后可以用 telnet localhost 10086 命令进行测试:

codroc:~$ telnet localhost 10086
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
1
0
Connection closed by foreign host.
codroc:~$ telnet localhost 10086
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
11
1
Connection closed by foreign host.
codroc:~$ telnet localhost 10086
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
1
12
posted @ 2020-11-26 14:10  Codroc  阅读(370)  评论(0编辑  收藏  举报