从零实现隧道穿透(一):socket编程记录

总览:

注意:

如果是买的云服务器,开放端口除了要在服务器上设置一下,还要再购买的网站的控制台处设置一下。

【客户端】

1、int socket(int family, int type, int protocol);

【作用】

创建一个套接字描述符,用 getaddrinfo 自动生成参数配合使用

【参数】

family:指明了协议族/域,通常AF_INET、AF_INET6、AF_LOCAL等
type:套接口类型,主要 SOCK_STREAM、SOCK_DGRAM、SOCK_RAW
protocol:一般取为0。成功时,返回一个小的非负整数值,与文件描述符类似

【返回值】

OK(非负),error(-1)

2、int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);

【作用】

客户端用来建立和服务器的连接(指定S端点地址),用 getaddrinfo 自动生成参数配合使用

【参数】

第一个参数是,通过 socket 得到的描述符,

【返回值】

OK(0),error(-1)

3、closesocket:

释放套接字

【服务器端】

1、int socket(int family, int type, int protocol);

【作用】

创建一个套接字描述符,用 getaddrinfo 自动生成参数配合使用

【参数】

family:指明了协议族/域,通常AF_INET、AF_INET6、AF_LOCAL等
type:套接口类型,主要 SOCK_STREAM、SOCK_DGRAM、SOCK_RAW
protocol:一般取为0。成功时,返回一个小的非负整数值,与文件描述符类似

【返回值】

OK(非负),error(-1)

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

【作用】

服务器用来绑定本地端点地址(创建 主套接字),用 getaddrinfo 自动生成参数配合使用

【返回值】

OK(0),error(-1)

3、int listen(int sockfd, int backlog);

【作用】

只用于TCP,将主动套接字转化为监听套接字,设置队列大小(backlog,通常设置为较大的值)

【返回值】

OK(0),error(-1)

4、int accept(int listenfd, struct sockaddr *addr, int *addrlen);

【作用】

只用于TCP,会创建一个新的套接字(和客户端关联起来了的套接字),使用这个新的套接字和客户端进行通信,用于并发。本函数会阻塞等待直到有客户端请求到达。

【参数】

addr:存放客户端的地址
addrlen:在调用函数时被设置为 addr 指向区域的长度,在函数调用结束后被设置为实际地址信息的长度

【返回值】

OK(新的 fd ),error(-1)
返回的 fd 是一个新的套接字描述符,它代表的是和客户端的新的连接,可以把它理解成是一个客户端的socket, 这个socket包含的是客户端的ip和port信息 。(当然这个new_socket会从sockfd中继承 服务器的ip和port信息,两种都有了),而参数中的SOCKET   s包含的是服务器的ip和port信息 。
之后的 send 和 recv 函数中的 fd 都是指这个 new_fd。

【getaddrinfo自动生成参数】

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

【作用】

主机名、主机地址、服务名、端口号的字符串表示 -> 套接字

【返回值】

返回值:OK(0), error(非0)

【参数】

hostname:主机名或地址串(IPv4的点分十进制串或者 IPv6的16进制串)
service:十进制的端口号,或是已定义的服务名称,如ftp、http等

hints:空指针,或是一个指向某个 addrinfo 结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。
举例来说:如果指定的服务既支持 TCP 也支持 UDP,那么调用者可以把 hints 结构中的 ai_socktype 成员设置成 SOCK_DGRAM 使得返回的仅仅是适用于数据报套接口的信息。
必须先分配一个 hints 结构,把它清零后填写需要的字段

result:一个指向 addrinfo 结构体链表的指针。用于后续的 socket 函数。默认最多返回 3 个 addrinfo 结构。调用 getaddrinfo 后遍历链表,逐个尝试每个返回地址。

hints 和 result 的结构:

struct addrinfo {
    int ai_flags;    // Hints argument flags
    int ai_family;   // AF_INET(ipv4)或 AF_INET6(ipv6)
    int ai_socktype; // SOCK_STREAM(TCP流)或 SOCK_DGRAM(UDP数据报)
    int ai_protocol; // 一般为 0 不做修改
    char *ai_canonname; // Canonical hostname
    size_t ai_addrlen;  //Size of ai_addr struct
    struct sockaddr *ai_addr; // Ptr to socket address structure
    struct addrinfo *ai_next; // Ptr to next item in linked list
};

其中,

struct sockaddr {
    uint16_t  sa_family;    // Protocol family
    char      sa_data[14];  // Address data
};

ai_flags:可以把各种值用 OR 组合起来得到该掩码。
hint.ai_flags常用:
AI_ADDRCONFIG:在使用连接时设置这个。
AI_CANONNAME:ai_canonname 默认为 NULL。设置这个标志后,将链表里第一个结构的 ai_canonname 指向主机的???
AI_NUMERICSERV:原本 getaddrinfo 第二个参数,可以为服务名或端口号,设置后强制为端口号。
AI_PASSIVE:返回的套接字地址为服务器监听套接字。此时 hostname 为 NULL

void freeaddrinfo(struct addrinfo *res);

用完必须释放。res 参数指向链表中第一个 addrinfo 结构。这个链表中的所有结构以及它们指向的任何动态存储空间都被释放掉。

const char *gai_strerror(int error);

打印错误信息:该函数以 getaddrinfo 返回的非 0 错误值的名字和含义为他的唯一参数,返回一个指向对应的出错信息串的指针。

其他还有:
getprotobyname:协议名->协议号
getservbyname:服务名->熟知端口号
但是我没用到。

【接收和发送数据】

1、int recv(int fd, char *buf, int len, int flags);

【作用】

客户端或服务器,接收数据。

【参数】

fd:接收端套接字描述符;
buf:缓冲区,该缓冲区用来存放 recv 函数接收到的数据
len:buf 的长度
flags:一般置 0

【返回值】

失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回接收数据的长度

2、int send(int fd, const char *buf, int len, int flags);

【作用】

发送数据

【参数】

fd:指定发送端套接字描述符
buf:要发送数据的缓冲区
len:实际要发送的数据的字节数
flags:一般置0

【返回值】

失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回发送数据的长度。

【字节顺序转换】

1、uint32_t htonl(uint32_t hostlong);

  • 主机字节顺序 转换为 网络字节顺序(32位)

2、uint32_t ntohl(uint32_t netlong);

  • 网络字节顺序 转换为 主机字节顺序(32位)

【ip格式转换】

1、int inet_pton(AF_INET, const char *src, void *dst);

  • 点分十进制ip -> 二进制网络字节顺序ip
  • Returns: 1 if OK, 0 if src is invalid dotted decimal, −1 on error

2、const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size):

  • 二进制网络字节顺序ip -> 点分十进制ip
  • size:目标存储单元的大小
  • Returns: pointer to a dotted-decimal string if OK, NULL on error
    注:可以是AF_INET(ipv4)也可以是AF_INET6(ipv6)

【套接字格式】

// IP socket address structure
struct sockaddr_in  {
    uint16_t sin_family;  // Protocol family (always AF_INET)
    uint16_t sin_port;    // Port number in network byte order
    struct in_addr sin_addr; // IP address in network byte order
    unsigned char sin_zero[8]; // Pad to sizeof(struct sockaddr)
};
// Generic socket address structure (for connect, bind, and accept)
struct sockaddr {
    uint16_t  sa_family;    // Protocol family
    char      sa_data[14];  // Address data
};

【域名与ip转换】

struct hostent *gethostbyname(const char *name);

  • 域名->32位ip地址(返回的是按网络字节顺序的)
  • 返回结构:
struct hostent {
    char    *h_name; // 主机的规范名
    char    **h_aliases; // 主机的别名
    int     h_addrtype; // 主机ip地址的类型ipv4(AF_INET),pv6(AF_INET6)
    int     h_length; // 主机ip地址的长度
    char    **h_addr_list; // 主机的ip地址(网络字节序),打印需要调用inet_ntop()
};

【inet_addr】

in_addr_t inet_addr(const char *cp);

将一个点分十进制的IP转换成一个长整数型数(u_long类型)

代码

服务器端开放 5678 端口,客户端在建立完连接后,服务器端给客户端发送信息,客户端接收信息并输出。

服务器端:

#include <cstdio>
#include <cstring>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXLINE 128

class Socks5Server {
public:
    Socks5Server();
    ~Socks5Server();
    int open_listenfd();
    int listen_client();
    void recv_mes();
    void send_mes(int fd, const char* str);

private:
    int _fd; // 主 fd
    char _buf[MAXLINE + 1];
};

Socks5Server::Socks5Server() {
    _fd = -1;
}

Socks5Server::~Socks5Server() {
    if (_fd > 0) {
        close(_fd);
    }
}

int Socks5Server::open_listenfd() {
    // 成功,返回0;失败,返回-1
    /*
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    服务器用来绑定本地端点地址(创建 主套接字),用 getaddrinfo 自动生成参数配合使用
    OK(0),error(-1)
    */
    struct addrinfo *p, *lisp, hints;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM; // TCP 会建立连接
    hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;
    hints.ai_flags |= AI_NUMERICSERV;
    /*
    hint.ai_flags 需要修改:
    AI_ADDRCONFIG:在使用连接时设置这个。
    AI_CANONNAME:ai_canonname 默认为 NULL。设置这个标志后,将链表里第一个结构的 ai_canonname 指向主机的???
    AI_NUMERICSERV:原本 getaddrinfo 第二个参数,可以为服务名或端口号,设置后强制为端口号。
    AI_PASSIVE:返回的套接字地址为服务器监听套接字。此时 hostname 为 NULL
    */
    int err = getaddrinfo(NULL, "5678", &hints, &lisp);
    if (err != 0) {
        printf("getaddrinfo error:%s\n", gai_strerror(err));
    }
    p = lisp;
    int optval = 1;
    for (p = lisp; p; p = p->ai_next) {
        _fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (_fd < 0) continue; // 看链表中下一个结构体
        /*
        在 TCP 连接中,recv 等函数默认为阻塞模式(block),即直到有数据到来之前函数不会返回,而我们有时则需要一种超时机制,使其在一定时间后返回,而不管是否有数据到来,这里我们就会用到:
        int  setsockopt(int fd, int level, int optname, void* optval, socklen_t* optlen);
        */
        setsockopt(_fd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));
        // bind 服务器用来绑定本地端点地址(创建 主套接字)
        err = bind(_fd, p->ai_addr, p->ai_addrlen);
        if (err == 0) { // 绑定成功
            if ((err = listen(_fd, 1024)) == -1) {
                printf("transfer error\n");
                close(_fd);
                //return -1;
            } else {
                //printf("_fd: %d\n", _fd);
                break; // 成功建立连接
            }
        }
    }
    freeaddrinfo(lisp);
    if (p == NULL) {
        printf("no addr\n");
        return -1; // 失败
    }
    else return 0; // 成功
}

int Socks5Server::listen_client() {
    /*
    int accept(int listenfd, struct sockaddr *addr, int *addrlen);
    只用于TCP,会创建一个新的套接字(和客户端关联起来了的套接字),使用这个新的套接字和客户端进行通信,用于并发。本函数会阻塞等待直到有客户端请求到达。
        addr:存放客户端的地址
        addrlen:在调用函数时被设置为 addr 指向区域的长度,在函数调用结束后被设置为实际地址信息的长度
    返回值:OK(新的 fd ),error(-1)
    */
    int new_fd;
    //struct sockaddr clientaddr;
    char clientaddr[1000];
    //socklen_t clientlen = sizeof(clientaddr);
    socklen_t clientlen = 1000;
    // 本函数会阻塞等待直到有客户端请求到达。
    new_fd = accept(_fd, (struct sockaddr *)&clientaddr, &clientlen);
    //printf("new_fd : %d\n", new_fd);
    //printf("%s\n",strerror(new_fd));
    return new_fd;
}

void Socks5Server::recv_mes() {
    /*
    int recv( int fd, char *buf, int len, int flags);
    客户端或服务器,接收数据。
        fd:接收端套接字描述符;
        buf:缓冲区,该缓冲区用来存放 recv 函数接收到的数据
        len:buf 的长度
        flags:一般置 0
    失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回接收数据的长度
    */
    int ret;
    while ((ret = recv(_fd, _buf, MAXLINE, 0)) > 0) {
        // 成功时,返回字符串长度
        _buf[ret] = '\0';
        printf("%s\n", _buf);
    }
}

void Socks5Server::send_mes(int fd, const char* str) {
    /*
    int send(int fd, const char *buf, int len, int flags);
        参数一:指定发送端套接字描述符;
        参数二:指明一个存放应用程序要发送数据的缓冲区;
        参数三:指明实际要发送的数据的字节数;
        参数四:一般置0;
    返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回发送数据的长度。
    */
    send(fd, str, strlen(str), 0);
}

int main() {
    Socks5Server s;
    int ret = s.open_listenfd();
    if (ret < 0) {
        printf("open_main_fd error\n");
    } else {
        printf("open_main_fd success\n");
        int tt = 3;
        while (tt--) {
            int new_fd = s.listen_client();
            if (new_fd == -1) {
                printf("new_fd error\n");
            } else {
                printf("new_fd success : %d\n", new_fd);
            }
            s.send_mes(new_fd, "hello\n");
            close(new_fd);
        }
    }
    return 0;
}

客户端:

#include <cstdio>
#include <cstring>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>


#define DONAME "xx" // 32位点分十进制 ip,这里我改掉了
#define MAXLINE 128

class Socks5Client {
public:
    Socks5Client(const char* hostname, const char* port);
    ~Socks5Client();
    int open_clientfd();
    void recv_mes();

private:
    int _fd;
    char _hostname[MAXLINE + 1];
    char _port[MAXLINE + 1];
    char _buf[MAXLINE + 1];
};

Socks5Client::Socks5Client(const char* hostname, const char* port) {
    _fd = -1;
    int len = strlen(hostname);
    if (len <= MAXLINE) {
        memcpy(_hostname, hostname, len);
        _hostname[len] = '\0';
        //printf("hostname : %s\n", _hostname);
    }
    len = strlen(port);
    if (len <= MAXLINE) {
        memcpy(_port, port, len);
        _port[len] = '\0';
        //printf("_port : %s\n", _port);
    }
}

Socks5Client::~Socks5Client() {
    if (_fd > 0) {
        close(_fd);
    }
}

int Socks5Client::open_clientfd() {
    // 成功 返回 0
    // 失败,返回 -1
    struct addrinfo *p, *lisp, hints;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM; // TCP 会建立连接
    // AI_ADDRCONFIG:在使用连接时设置这个。
    // AI_NUMERICSERV:不可为服务名,强制为端口号。
    //hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;
    hints.ai_flags = AI_ADDRCONFIG;
    int err = getaddrinfo(_hostname, _port, &hints, &lisp);
    //printf("hostname: %s port: %s\n", _hostname, _port);

    if (err != 0) {
        printf("getaddrinfo error:%s\n", gai_strerror(err));
    }

    p = lisp;
    for (p = lisp; p; p = p->ai_next) {
        //printf("p: %d\n", p);
        _fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        //printf("fd : %d\n", _fd);
        if (_fd < 0) {
            printf("error fd : %d\n", _fd);
            continue; // 看链表中下一个结构体
        }
        /*
        int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
        客户端用来建立和服务器的连接(指定S端点地址),返回值:OK(0),error(-1)
        */
        err = connect(_fd, p->ai_addr, p->ai_addrlen);

        if (err == 0) {
            printf("connect success\n");
            break; // 成功建立连接
        } else {
            perror("connect error: ");
            close(_fd); // 建立连接失败
        }
    }

    freeaddrinfo(lisp);
    if (p == NULL) {
        printf("no addr\n");
        return -1; // 失败
    } else return 0; // 成功
}
/*
int getaddrinfo(const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result);
    主机名、主机地址、服务名、端口号的字符串表示 -> 套接字
    返回值:OK(0), error(非0)

    参数:
    hostname:主机名或地址串(IPv4的点分十进制串或者 IPv6的16进制串)
    service:十进制的端口号,或是已定义的服务名称,如ftp、http等

    hints:空指针,或是一个指向某个 addrinfo 结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。
    举例来说:如果指定的服务既支持 TCP 也支持 UDP,那么调用者可以把 hints 结构中的 ai_socktype 成员设置成 SOCK_DGRAM 使得返回的仅仅是适用于数据报套接口的信息。
    必须先分配一个 hints 结构,把它清零后填写需要的字段

    result:一个指向 addrinfo 结构体链表的指针。用于后续的 socket 函数。默认最多返回 3 个 addrinfo 结构。调用 getaddrinfo 后遍历链表,逐个尝试每个返回地址。

hints 和 result 的结构:
struct addrinfo {
    int ai_flags;    // Hints argument flags
    int ai_family;   // AF_INET(ipv4)或 AF_INET6(ipv6)
    int ai_socktype; // SOCK_STREAM(TCP流)或 SOCK_DGRAM(UDP数据报)
    int ai_protocol; // 一般为 0 不做修改
    char *ai_canonname; // Canonical hostname
    size_t ai_addrlen;  //Size of ai_addr struct
    struct sockaddr *ai_addr; // Ptr to socket address structure
    struct addrinfo *ai_next; // Ptr to next item in linked list
};

ai_flags:可以把各种值用 OR 组合起来得到该掩码。

用完必须释放:
void freeaddrinfo(struct addrinfo *res);
    res 参数指向链表中第一个 addrinfo 结构。这个链表中的所有结构以及它们指向的任何动态存储空间都被释放掉。


const char *gai_strerror(int error);
    打印错误信息:该函数以 getaddrinfo 返回的非 0 错误值的名字和含义为他的唯一参数,返回一个指向对应的出错信息串的指针。
*/

void Socks5Client::recv_mes() {
    int ret;

    while ((ret = recv(_fd, _buf, MAXLINE, 0)) > 0) {
        printf("recv success\n");
        // 成功时,返回字符串长度
        _buf[ret] = '\0';
        printf("str: %s\n", _buf);
        //ret = recv(_fd, _buf, MAXLINE, 0);
    }
}

int main() {
    Socks5Client c(DONAME, "5678");
    int ret = c.open_clientfd();
    if (ret < 0) {
        printf("open_clientfd error\n");
    } else {
        printf("open_clientfd success\n");
        c.recv_mes();
    }
    return 0;
}

并发编程

1、每次 accept 都在 fork 的子进程里实现

略微修改服务器端的代码即可:

void sigchld_handler(int sig) {
    while (waitpid(-1, 0, WNOHANG) > 0) {
        /*
        回收子进程,回收成功一个,返回子进程的pid
        WNOHANG:非阻塞。如果有子进程,但没有结束(没有变僵尸进程),waitpid 返回 0,退出 while
        如果有僵尸进程,回收一个僵尸进程,返回僵尸进程 pid,所以要用循环去处理,把僵尸进程回收完
        */
        ;
    }
    //printf("wait end\n");
}

int Socks5Server::listen_client() {
    signal(SIGCHLD, sigchld_handler);
    printf("listening=========== main fd : %d\n", _fd);

    int new_fd, num = 0;
    char clientaddr[1000];
    socklen_t clientlen = 1000;

    while(1) {
        // 本函数会阻塞等待直到有客户端请求到达。
        new_fd = accept(_fd, (struct sockaddr *)&clientaddr, &clientlen);
        //printf("new_fd : %d\n", new_fd);
        num++;

        if (fork() == 0) {
            // 子进程
            close(_fd);
            if (new_fd == -1) {
                printf("new_fd error\n");
            } else {
                printf("【child】new_fd success : %d\n", new_fd);
            }
            send_mes(new_fd, "hello\n");
            printf("num : %d\n", num);

            close(new_fd);
            //printf("【child】close new_fd : %d\n", new_fd);
            exit(0); // 子进程直接退出,父进程会重新进入循环
        } else {
            // 父进程
            //printf("【father】close new_fd : %d\n", new_fd);
            close(new_fd);
        }

    }
    return new_fd;
}

int main() {
    Socks5Server s;
    int ret = s.open_listenfd();
    if (ret < 0) {
        printf("open_main_fd error\n");
    } else {
        printf("open_main_fd success\n");
        s.listen_client();
    }
    return 0;
}

哈哈哈哈哈哈