C++网络编程入门学习(二)-- socket通信

C++网络编程入门学习(二)-- socket通信

学习地址

Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。

网络通信的主体主要分为两部分:客户端服务器端。在客户端和服务器通信的时候需要频繁提到三个概念:IP端口通信数据,下面介绍一下需要注意的一些细节问题。

大小端转化函数

BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。

#include <arpa/inet.h>
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int

// 这套api主要用于 网络通信过程中 IP 和 端口 的 转换
// 将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);	
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);	

// 将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。

IP地址转换

虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:

// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst); 
  • 参数:

    • af: 地址族(IP地址的家族包括ipv4和ipv6)协议

      (其中,AF_INET: ipv4格式的ip地址;AF_INET6: ipv6格式的ip地址)

    • src:传入参数,对应要转换的点分十进制的ip地址:192.168.1.100

    • dst:传出参数,函数调用完成,转换得到的大端整形ip被写入到这块内存中

  • 返回值:成功返回1,失败返回0或者-1

#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址        
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 参数:

    • af:地址族协议

      (其中,AF_INET: ipv4格式的ip地址;AF_INET6: ipv6格式的ip地址)

    • src:传入参数,这个指针指向的内存中存储了大端的整形IP地址

    • dst:传出参数,存储转换得到的小端的点分十进制的IP地址

    • size:修饰dst参数,标记dst指向内存中最多可以存储多少个字节

  • 返回值:

    • 成功:指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
    • 失败:NULL

还有一组函数也能进程IP地址大小端的转换,但是只能处理ipv4的ip地址:

// 点分十进制IP -> 大端整形
in_addr_t inet_addr (const char *cp);

// 大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in);

sockaddr 数据结构

image-20240913172821173

// 在写数据的时候不好用
struct sockaddr {
	sa_family_t sa_family;       // 地址族协议, ipv4
	char        sa_data[14];     // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}

typedef unsigned short  uint16_t;
typedef unsigned int    uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

struct in_addr
{
    in_addr_t s_addr;
};  

// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
    sa_family_t sin_family;		/* 地址族协议: AF_INET */
    in_port_t sin_port;         /* 端口, 2字节-> 大端  */
    struct in_addr sin_addr;    /* IP地址, 4字节 -> 大端  */
    /* 填充 8字节 */
    unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
               sizeof (in_port_t) - sizeof (struct in_addr)];
};  

套接字函数

使用套接字通信函数需要包含头文件<arpa/inet.h>,包含了这个头文件<sys/socket.h>就不用在包含了。

// 创建一个套接字
int socket(int domain, int type, int protocol);
  • 参数:
    • domain: 使用的地址族协议
      • AF_INET: 使用IPv4格式的ip地址
      • AF_INET6: 使用IPv6格式的ip地址
    • type:
      • SOCK_STREAM: 使用流式的传输协议
      • SOCK_DGRAM: 使用报式(报文)的传输协议
    • protocol: 一般写0即可, 使用默认的协议
      • SOCK_STREAM: 流式传输默认使用的是tcp
      • SOCK_DGRAM: 报式传输默认使用的udp
  • 返回值:
    • 成功: 可用于套接字通信的文件描述符
    • 失败: -1

函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。

// 将文件描述符和本地的IP与端口进行绑定   
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数:
    • sockfd: 监听的文件描述符, 通过socket()调用得到的返回值
    • addr: 传入参数, 要绑定的IP和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
    • addrlen: 参数addr指向的内存大小, sizeof(struct sockaddr)
  • 返回值:成功返回0,失败返回-1
// 给监听的套接字设置监听
int listen(int sockfd, int backlog);
  • 参数:
    • sockfd: 文件描述符, 可以通过调用socket()得到,在监听之前必须要绑定 bind()
    • backlog: 同时能处理的最大连接要求,最大值为128
  • 返回值:函数调用成功返回0,调用失败返回 -1
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)		
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数:
    • sockfd: 监听的文件描述符
    • addr: 传出参数, 里边存储了建立连接的客户端的地址信息
    • addrlen: 传入传出参数,用于存储addr指向的内存大小
  • 返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1

这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。

// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
  • 参数:
    • sockfd: 用于通信的文件描述符, accept() 函数的返回值
    • buf: 指向一块有效内存, 用于存储接收是数据
    • size: 参数buf指向的内存的容量
    • flags: 特殊的属性, 一般不使用, 指定为 0
  • 返回值:
    • 大于0:实际接收的字节数
    • 等于0:对方断开了连接
    • -1:接收数据失败了

如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0

// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
  • 参数:
    • fd: 通信的文件描述符, accept() 函数的返回值
    • buf: 传入参数, 要发送的字符串
    • len: 要发送的字符串的长度
    • flags: 特殊的属性, 一般不使用, 指定为 0
  • 返回值:
    • 大于0:实际发送的字节数,和参数len是相等的
    • -1:发送数据失败了
// 成功连接服务器之后, 客户端会自动随机绑定一个端口,而服务器端的bind()函数则是用于绑定ip和端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数:
    • sockfd: 通信的文件描述符, 通过调用socket()函数就得到了
    • addr: 存储了要连接的服务器端的地址信息: iP 和 端口,这个IP和端口也需要转换为大端然后再赋值
    • addrlen: addr指针指向的内存的大小 sizeof(struct sockaddr)
  • 返回值:连接成功返回0,连接失败返回-1

TCP通信

image-20240913192855937

基于TCP的服务器端通信代码

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

int main()
{
    // 1. 创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 将socket()返回值和本地的IP端口绑定到一起
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);   // 大端端口
    // INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
    // 这个宏可以代表任意一个IP地址
    // 这个宏一般用于本地的绑定操作
    addr.sin_addr.s_addr = INADDR_ANY;  // 这个宏的值为0 == 0.0.0.0
//    inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }

    // 3. 设置监听
    ret = listen(lfd, 128);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }

    // 4. 阻塞等待并接受客户端连接
    struct sockaddr_in cliaddr;
    int clilen = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
    if(cfd == -1)
    {
        perror("accept");
        exit(0);
    }
    // 打印客户端的地址信息
    char ip[24] = {0};
    printf("客户端的IP地址: %s, 端口: %d\n",
           inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)),
           ntohs(cliaddr.sin_port));

    // 5. 和客户端通信
    while(1)
    {
        // 接收数据
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        int len = read(cfd, buf, sizeof(buf));
        if(len > 0)
        {
            printf("客户端say: %s\n", buf);
            write(cfd, buf, len);
        }
        else if(len  == 0)
        {
            printf("客户端断开了连接...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    close(cfd);
    close(lfd);

    return 0;
}

基于TCP通信的客户端通信代码

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

int main()
{
    // 1. 创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 连接服务器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);   // 大端端口
    inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);

    int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("connect");
        exit(0);
    }

    // 3. 和服务器端通信
    int number = 0;
    while(1)
    {
        // 发送数据
        char buf[1024];
        sprintf(buf, "你好, 服务器...%d\n", number++);
        write(fd, buf, strlen(buf)+1);
        
        // 接收数据
        memset(buf, 0, sizeof(buf));
        int len = read(fd, buf, sizeof(buf));
        if(len > 0)
        {
            printf("服务器say: %s\n", buf);
        }
        else if(len  == 0)
        {
            printf("服务器断开了连接...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
        sleep(1);   // 每隔1s发送一条数据
    }

    close(fd);

    return 0;
}

文件描述符在套接字通信中的作用

文件描述符对应的内存结构:

  • 一个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区
    • 读数据: 通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区
    • 写数据: 通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区

监听的文件描述符:

  • 客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中
  • 读缓冲区中有数据, 说明有新的客户端连接
  • 调用accept()函数, 这个函数会检测监听文件描述符的读缓冲区
    • 检测不到数据, 该函数阻塞
    • 如果检测到数据, 解除阻塞, 新的连接建立

通信的文件描述符:

  • 客户端和服务器端都有通信的文件描述符
  • 发送数据:调用函数 write() / send(),数据进入到内核中
    • 数据并没有被发送出去, 而是将数据写入到了通信的文件描述符对应的写缓冲区中
    • 内核检测到通信的文件描述符写缓冲区中有数据, 内核会将数据发送到网络中
  • 接收数据: 调用的函数 read() / recv(), 从内核读数据
    • 数据如何进入到内核程序猿不需要处理, 数据进入到通信的文件描述符的读缓冲区中
    • 数据进入到内核, 必须使用通信的文件描述符, 将数据从读缓冲区中读出即可

基于多线程的并发服务器实现

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<pthread.h> // 多线程的头文件

// 定义一个信息结构体
struct SockInfo
{
    struct sockaddr_in addr; // 存储地址信息
    int fd; // 存储文件描述符
};

struct SockInfo infos[512]; // 最多支持和512个客户端通信

void* working(void* arg);

int main() { // 主线程的处理流程
    // 1.创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0); // 使用TCP,因此用流式的
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    // 2. 绑定本地的IP 和 port
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999); // 绑定一个未被占用的端口
    // 如果我们给监听的套接字绑定一个0地址,就意味着它会自动地去读网卡对应的实际IP地址
    saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0。 0地址的意思是可以绑定本地的任意一个IP地址
    int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        return -1;
    }

    // 3. 设置监听
    ret = listen(fd, 128); // 可以设置一个比128小的整数,最大为128
    if(ret == -1) {
        perror("listen");
        return -1;
    }

    // 初始化结构体数组
    int max = sizeof(infos) / sizeof(infos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&infos[i], sizeof(infos[i])); // 将每个元素初始化为0
        infos[i].fd = -1;
    }

    // 4. 阻塞并等待客户端的连接
    int addrlen = sizeof(struct sockaddr_in);
    while(1) {
        struct SockInfo* pinfo;
        for(int i = 0; i < max; i++) {
            if(infos[i].fd == -1) {
                pinfo = &infos[i];
                break;
            }
        }
        int cfd = accept(fd, (struct sockaddr*)&pinfo->addr, &addrlen);
        pinfo->fd = cfd;
        if(cfd == -1) {
            perror("accept");
            break;
        }
        // 创建子线程
        pthread_t tid; // 传出参数,存储创建出的子线程的线程id
        pthread_create(&tid, NULL, working, pinfo);
        pthread_detach(tid); 
    }

    // cfd不需要关闭,是因为在子线程需要被使用
    close(fd); 

    return 0;
}

void* working(void* arg) { // 子线程的任务函数

    struct SockInfo* pinfo = (struct SockInfo*)arg;

    // 连接建立成功,打印客户端的IP和端口信息
    char ip[32];
    printf("client IP: %s, port: %d\n", 
            inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)),
            ntohs(pinfo->addr.sin_port));

    // 通信
    while(1) {
        // 接收数据
        char buff[1024];
        int len = recv(pinfo->fd, buff, sizeof(buff), 0);
        if(len > 0) {
            printf("client say: %s\n", buff);
            send(pinfo->fd, buff, len, 0);
        }
        else if(len == 0) { // 客户端断开连接
            printf("Client disconnects....\n");
            break;
        }
        else {
            perror("recv");
            break;
        }
    }

    // 关闭文件描述符
    close(pinfo->fd);
    pinfo->fd = -1;

    return NULL;
}

image-20240919164258722

4b969ad66f9cb7154c9c34baa5f6bb82

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值