【学习记录】C/C++网络编程初步

Sunday, June 4, 2023
本文共2142字
5分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95cc++%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%88%9D%E6%AD%A5/。商业转载请联系作者获得授权,非商业转载请注明出处!

One may say the eternal mystery of the world is its comprehensibility. — Albert Einstein

学习资料

非常感谢大佬的教程~简单易懂~我推荐和我一样打算入门socket并快速上手的同学都看一看~

相关的理论知识在这些资料里面已经说的很清楚了,我这里就不再赘述了,仅仅简单地说一说TCP协议下的通信流程

通信流程

是时候祭出这张图了,图源https://subingwen.cn/linux/socket/tcp.jpg

文内图片

简单来说,就是服务器要先建立套接字(表现为一个用于监听的文件描述符)并将socket文件描述符绑定连接端口,然后监听连接端口是否有新的客户端试图连接(listen设置端口监听,accept阻塞地等待客户端连接),然后假如有客户端通过这个端口连接,accept就取消阻塞,同时设置一个结构体(struct sockaddr_in),我们可以通过这个结构体知道客户端的相关信息;同时accept还会返回一个用于通信的文件描述符,我们可以通过这个文件描述符来与客户端使用recv/send通信。最后如果客户端有序关闭了(orderly shutdown)了,那么recv就会返回0,这时我们就知道客户端结束连接了(如果出现错误就会返回-1并设置errno)

然后客户端就比较简单,只需要建立socket文件描述符,在通过connect连接既定的IP:port后直接向这个socket文件描述符进行读写(recv/send)就可以了

如何查看相关API

非常建议使用Linux的man命令查看!虽然是英文的,但是对于正在使用的系统是绝对权威的,而且无需联网,速度很快!

如果你想使用vim查看man手册(因为你的vim可能设置了习惯的快捷键),在此自荐我写的脚本,可以使用vim在man、tldr、cppman之间查询:使用vim在man、tldr、cppman库间搜索帮助手册并打开 - P3troL1er 的个人博客

完整代码

因为我写了详细的注释,所以我就不再赘述一遍了,如果对api有疑问建议使用man手册查看

下面是完整的C语言代码,本来是想写C++代码的,但是由于教程是C语言的,不知不觉几乎完全是C-style的代码,索性就直接写成C语言代码了

server.c:

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>

// 几个宏用来防止手动写入过多错误判断代码
#define CKE(EXP) do {\
    if ((EXP) == -1) {\
        fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno));\
        exit(127+errno);\
    }\
} while(0)

#define CKEN(EXP) do {\
    if ((EXP) != 0) {\
        fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno));\
        exit(127+errno);\
    }\
} while(0)

// 用来保存通信线程所需的相关信息
typedef struct{
    int cfd; // 通信的文件描述符
    struct sockaddr_in details; // 客户端的相关信息
    socklen_t details_len; // 上面那个结构体的长度,如果通信线程调用recvfrom就可以用到
    char ip[16]; // 保存客户端的IP字符串
    int port; // 保存客户端的端口
} netfd;

static void* communicate(void* arg){
    // 通过强制转换
    netfd* netinfo = (netfd*)arg;
    // 每个已连接的通信线程都有1kb的缓冲区
    char buf[1024] = {0};
    // 固定的回复
    const char* response = "server's spell...";
    // TODOrecv & send
    while (true) {
        // 获取收到的字节数量
        int len = recv(netinfo->cfd, buf, sizeof(buf), 0);
        /**
        关于recv的返回值,可以参考Linux的man手册:
       These calls return the number of bytes received, or -1 if an error occurred.  In the event of an error, errno is set to indicate the error.

       When a stream socket peer has performed an orderly shutdown, the return value will be 0 (the traditional "end-of-file"  return).

       Datagram  sockets in various domains (e.g., the UNIX and Internet domains) permit zero-length datagrams.  When such a datagram is received, the return value is 0.

       The value 0 may also be returned if the requested number of bytes to receive from a stream socket was 0.
        */
        if (len > 0)
        {
            printf("client from %s:%d sent: %s\n", netinfo->ip, netinfo->port, buf);
            send(netinfo->cfd, response, sizeof(char) * (strlen(response) + 1), 0);
        } else if (len == 0) {
            // TODO接收到客户端终止连接请求
            puts("the client has ended the connection...");
            break;
        } else {
            // 接收到错误就打印出来,并且退出线程
            fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
            close(netinfo->cfd);
            pthread_exit((void*)((long)127+errno));
        }
        // 一定要设置buf为0,不然可能和新信息混杂在一起
        // 因为buf被初始化为0,所以memset放在循环末尾
        memset(buf, 0, sizeof(buf));
    }
    // 当客户端正常退出时就关闭通信的文件描述符
    close(netinfo->cfd);
    pthread_exit((void*)((long)0));
}

static void sig_int(int signo){
    printf("thread %ld received: %s\n", pthread_self(), strsignal(signo));
    puts("server exiting...");
    exit(signo);
}

int main(int argc, char const *argv[])
{
    // 声明局部变量
    int lfd;
    pthread_attr_t tid_attr;
    pthread_t tid;
    int err;
    struct sockaddr_in server_addr; // 保存服务器的相关信息
    const int connection_num = 128; // 设置最大连接数
    // 设置信号处理函数
    signal(SIGINT, sig_int);
    // TODO设置监听的套接字
    // 设置TCP协议
    CKE(lfd = socket(AF_INET, SOCK_STREAM, 0));
    // TODO绑定端口
    server_addr.sin_family = AF_INET;
    // from host byte order to network byte order
    server_addr.sin_port = htons(10000);
    // 代表本机的任意地址
    // 或者设置其中一个IP,使用inet_pton将IP地址字符串转换为二进制格式
    // inet_pton - convert IPv4 and IPv6 addresses from text to binary form
    server_addr.sin_addr.s_addr = INADDR_ANY;
    CKE(bind(lfd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
    // TODO设置端口监听
    CKE(listen(lfd, connection_num));
    // 初始化线程属性,设置为线程启动即分离
    CKEN((err = pthread_attr_init(&tid_attr)));
    CKEN((err = pthread_attr_setdetachstate(&tid_attr, PTHREAD_CREATE_DETACHED)));
    // TODO开始接收连接请求
    // 阻塞直到有客户端连接
    // 在栈空间中保存该结构体的大小,因为后面的api需要传指针进去,可能会更改这个值
    // 不停地检测是否有连接
    // 如果没有连接就阻塞
    while (true) {
        // 这个结构体包括了传给线程的参数,因此不能是局部变量,可以在堆上分配
        netfd* netinfo = (netfd*)malloc(sizeof(netfd));
        netinfo->details_len = sizeof(netinfo->details);
        /**
        On  success,  these system calls return a file descriptor for the accepted socket (a nonnegative integer).  On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
        */
        CKE(netinfo->cfd = accept(lfd, (struct sockaddr*)&(netinfo->details), &(netinfo->details_len)));
        // 此时应该已经收到了客户端的相关信息
        netinfo->port = ntohs(netinfo->details.sin_port);
        // 打印客户端相关信息
        printf(
            "client's IP: %s, port: %d\n", 
            inet_ntop(AF_INET, &(netinfo->details.sin_addr.s_addr), netinfo->ip, netinfo->details_len), 
            netinfo->port
        );
        // 创建信息
        CKEN(err = pthread_create(&tid, &tid_attr, communicate, (void*)netinfo));
    }
    // TODO关闭两个端口
    // 在主线程只用关闭监听的文件描述符
    close(lfd);
    return 0;
}

client.c:

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define CKE(EXP) do {\
    if ((EXP) == -1) {\
        fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno));\
        exit(127+errno);\
    }\
} while(0)

int main(int argc, char const *argv[])
{
    // 声明局部变量
    int fd;
    char buf[1024] = {0};
    // TODO 建立套接字
    CKE(fd = socket(AF_INET, SOCK_STREAM, 0));
    // TODO 建立连接
    // 设置服务器地址、端口
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(10000);
    inet_pton(AF_INET, "127.0.0.1", &(server_addr.sin_addr.s_addr));
    CKE(connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
    //TODO send & recv
    for(int i = 0; i < 100; i++)
    {
        int num = snprintf(buf, sizeof(buf)/sizeof(char)-1, "Massage NO.%d", i);
        send(fd, buf, sizeof(char)*(num+1), 0);
        memset(buf, 0, sizeof(buf));
        if ((num = recv(fd, buf, sizeof(buf), 0)) > 0)
        {
            printf("server respond: %s\n", buf);
        }else if (num == 0) {
            puts("the server has ended its service...");
            break;
        }else {
            fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
            exit(127+errno);
        }
        sleep(1);
    }
    // TODO 关闭连接
    close(fd);
    return 0;
}

效果图:

文内图片