Linux socket聊天室

本文详细介绍了如何构建一个基于LinuxSocket的多线程聊天室,包括服务端和客户端的代码实现。服务端处理客户端的注册、登录、公聊、私聊等操作,采用多线程模型确保并发处理。客户端则提供用户交互界面,支持各种功能操作。
摘要由CSDN通过智能技术生成

目录

一、运行效果

1、分别编译客户端和服务端代码

2、运行

3、使用效果

 二、代码

chat.h

服务端代码 

客户端代码


一、运行效果

1、分别编译客户端和服务端代码

gcc client.c -o C -lpthread

gcc server.c -o S -lpthread

2、运行

先运行服务器端,8888为端口号

./S 8888

 再运行客户端,这里创建两个客户端,端口号要和服务端的一样

./C 127.0.0.1 8888

         可以看到,左下的窗口运之后,就会进入注册界面;而服务器也会提示有客户端的ip连接进来,这个时候再用右边的窗口运行客户端

  进入两个主页之后,服务器就会有不同的port对应不同的客户端

3、使用效果

注册

         上图可以看到,左下进行了一个注册之后就会提示注册成功,服务器上面也会有记录,下面进行登录

         上图左下登录之后会提示登录成功,服务器也会有记录,而左下的窗口只要再按下回车就能进入到功能主页。下面再用左下的窗口注册一个用户2

         上图可以看到,用户2注册成功登录之后,进入功能主页的左下用户也会有提示说右下用户上线了,服务器也会记录。下面左下用户进行“公聊”,只要是在线的用户都会收到信息

         上图坐下用户输入数字3,选择功能之后,就会提示输入,输入之后按回车,右下用户就出现了信息(蓝色字体),如下 

         上图可以看到右下用户出现了“公聊信息”蓝色字体,下面用右边的用户对左边用户进行“私聊”

         输入4选择功能之后,会询问对谁发信息,接着就输入用户的名字,在输入发送的消息,按下回车就可以发送,如下

        可以看到坐下的用户已经收到私聊的信息,这样就不会像公聊那样谁都能看到,输入5可以查看有谁在线,下面先把右下的用户退出

上图可以看到只查询到自己用户1的名字在线,下面用第二个用户查询

 上图可以看到,用户2上线之后,查到两个用户在线

 二、代码

chat.h

#ifndef __CHAT_H
#define __CHAT_H

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERVER_PORT 8888
#define SIZE 32
/*最大的用户数量*/
#define MAX_USER_NUM 64
/*在线用户*/
struct ONLINE{
    int fd;                     /*描述符,-1:该用户下线   >0:该用户已经登录,对应的套接字*/
    int flage;                  /*注册标志,-1 该条目没有用户信息  1:该条目有用户注册信息*/
    char name[SIZE];            /*姓名*/
    char password[SIZE];        /*密码*/
};
/*C/S通信的结构体*/
struct protocol{
    int cmd;                    /*命令*/
    int state;                  /*存储命令返回信息*/
    char name[SIZE];            /*用户姓名*/
    char data[MAX_USER_NUM];    /*数据*/
};
/*cmd(命令类型)*/
#define BROADCAST       0X00000001          /*广播数据*/
#define PRIVATE         0X00000002          /*私聊*/
#define REGISTE         0X00000004          /*注册*/
#define LOGIN           0X00000008          /*登录*/
#define ONLINEUSER      0X00000010          /*显示在线用户*/
#define LOGOUT          0X00000020          /*退出*/

/*return code(服务器处理结果返回值)*/
#define OP_OK               0X80000000      /*操作成功*/
#define ONLINEUSER_OK       0X80000001      /*显示在线用户,未结束*/
#define ONLINEUSER_OVER     0X88888888      /*显示在线用户,已经发送完毕*/
#define NAME_EXIST          0X80000003      /*注册信息,用户名已经存在*/
#define NAME_PWD_NMATCH     0X80000004      /*登录时,输入的用户名密码不对 */
#define USER_LOGED          0X80000005      /*登录时,提示该用户已经在线*/
#define USER_NOT_REGIST     0X80000006      /*登录时,提示用户没有注册*/

#endif


服务端代码 

#include "chat.h"
#include <stdio.h>
#include <unistd.h>

struct ONLINE online[MAX_USER_NUM];
/*将用户注册信息存储到在线用户列表中*/
int add_user(int socket_fd, struct protocol *msg) {
  int i, index = -1;
  char buf[128] = {0};
  /*历在线用户列表数组,从第一个开始找没有被占用的位置,将用户的注册信息存储到该位置*/
  for (i = 0; i < MAX_USER_NUM; i++) { /*判断当前位置是否已经被占用*/
    if (online[i].flage == -1) {       /* 标记该位置为已占用*/
      online[i].flage = 1;
      strcpy(online[i].name, msg->name);
      strcpy(online[i].password, msg->data);
      printf("regist %s to %d \n", msg->name, i);
      index = i;
      return index; /* 返回存储用户信息的位置*/
    }
  }
  return index;
}
/*从在线列表中删除
 *在聊天室程序中将在线用户列表中指定位置的客户端信息删除,
 *并向所有在线客户端发送某用户下线通知
 */
void del_user_online(int index) {
  int i;
  char buf[128] = {0};
  /*通过比较传入的 index 变量与 0 的大小,来判断传入参数是否合法
   *如果 index 小于 0,代表传入参数无效,直接返回
   */
  if (index < 0)
    return;
  /*删除在线用户列表中相应位置的客户端信息
   *将在线用户列表中指定位置的客户端信息的 fd 字段设置为 -1,表示该客户端已下线
   */
  online[index].fd = -1;
  /*通知所有客户端,某个用户下线了
   *遍历在线用户列表中的每个客户端,fd 不为 -1
   *的客户端,向其发送某用户下线的通知消息buf
   */
  sprintf(buf, "%s offline\n", online[index].name);
  for (i = 0; i < MAX_USER_NUM; i++) {
    if (online[i].fd == -1) {
      continue;
    }
    write(online[i].fd, buf, strlen(buf));
  }
  return;
}
/*向所有在线用户广播消息*/
void broadcast(int index, struct protocol *msg) {
  int i;
  char buf[128] = {0};
  sprintf(buf, "\033[0;34m\t%s say:%s\33[0m\n", online[index].name, msg->data);
  for (i = 0; i < MAX_USER_NUM; i++) {
    if ((online[i].fd == -1) || (i == index)) /*跳过不在线的和自己*/
    {
      continue;
    }
    write(online[i].fd, buf, strlen(buf));
  }
}
/*用于判断准备登录的用户是否已经登录,并返回其在列表中的索引*/
int find_dest_user_online(int socket_fd, int *index, struct protocol *msg) {
  int i;
  /*遍历在线用户列表数组*/
  for (i = 0; i < MAX_USER_NUM; i++) {
    if (online[i].flage == -1) {
      continue;
    }
    /*如果用户名和密码都匹配*/
    if ((strcmp(msg->name, online[i].name) == 0) &&
        (strcmp(msg->data, online[i].password) == 0)) 
    {/*不在线的先建立服务器连接*/
      if (online[i].fd == -1) {
        online[i].fd = socket_fd;
        *index = i;
        return OP_OK;
      } else {/*在线的打印已经登陆在线*/
        printf("%s had login\n", online[i].name);
        return USER_LOGED;/*用户已经登陆*/
      }
    }
  }/*遍历完所有在线用户仍然没有找到匹配的用户*/
  return NAME_PWD_NMATCH;
}
/*查找在线用户列表 online 中指定用户名*/
int find_dest_user(char *name) {
  int i;
  for (i = 0; i < MAX_USER_NUM; i++) {
    if (online[i].flage == -1) {
      continue;
    }
    if (strcmp(name, online[i].name) == 0) {
      return i;
    }
  }
  return -1;
}
/*私聊功能*/
void private(int index, struct protocol *msg) {
  int dest_index;
  char buf[128];
  /*查找目标用户在在线用户列表 online 中的索引*/
  dest_index = find_dest_user(msg->name);
  if (dest_index == -1) {
    /*向发送私聊请求的用户发送一条提示消息,告诉其该用户不在线*/
    sprintf(buf, "there is no  user :%s \n", msg->name);
    write(online[index].fd, buf, strlen(buf));
    return;
  } else {
    sprintf(buf, "\033[0;34m\t%s say to %s:%s\33[0m\n", online[index].name,
                                                online[dest_index].name,
                                                msg->data);
    write(online[dest_index].fd, buf, strlen(buf));
    return;
  }
}
/*列出在线用户*/
void list_online_user(int index) {
  int i;
  struct protocol msg;
  for (i = 0; i < MAX_USER_NUM; i++) {
    /*如果该套接字已经关闭,则继续下一个循环*/
    if (online[i].fd == -1) {
      continue;
    }
    memset(&msg, 0, sizeof(msg));
    msg.cmd = ONLINEUSER;
    msg.state = ONLINEUSER_OK;
    strcpy(msg.name, online[i].name);
    printf("list online[%d].name =%s \n",i, msg.name);
     /*向客户端发送在线用户结构体的数据*/
    write(online[index].fd, &msg, sizeof(msg));
    sleep(1);
  }
  /*表示在线用户列表已经全部发送完毕*/
  msg.cmd = ONLINEUSER;
  msg.state = ONLINEUSER_OVER;
  /*用于通知客户端当前任务已完成,并结束本次传输*/
  write(online[index].fd, &msg, sizeof(msg));
}
/*注册*/
void registe(int socket_fd, int *index, struct protocol *msg) {
  int dest_index;
  char buf[128];
  struct protocol msg_back;
  msg_back.cmd = REGISTE;
  /*查找该用户名是否已经被其他用户注册*/
  dest_index = find_dest_user(msg->name);
  if (dest_index == -1) {
    *index = add_user(socket_fd, msg);
    online[*index].flage = 1;
    msg_back.state =  OP_OK;
    printf("user %s regist success!\n", msg->name);
    write(socket_fd, &msg_back, sizeof(msg_back));
    return;
  } else {/*用户名已存在*/
    msg_back.state = NAME_EXIST;
    printf("user %s exist!\n", msg->name);
    write(socket_fd, &msg_back, sizeof(msg_back));
    return;
  }
}
/*登录*/
void login(int socket_fd, int *index, struct protocol *msg) {
  int i, ret;
  char buf[128];
  struct protocol msg_back;
  msg_back.cmd = LOGIN;
  /*查找该用户名是否已经在线*/
  ret = find_dest_user_online(socket_fd, index, msg);
  if (ret != OP_OK) {/*不等于表示该用户名不存在或者该用户未登录*/
    msg_back.state = ret;
    strcpy(buf, " no this user\n");
    printf("user %s login fail!\n", msg->name);
    write(socket_fd, &msg_back, sizeof(msg_back));
    return;
  } else {/*登录成功*/
    msg_back.state = OP_OK;
    strcpy(msg_back.data, "login success\n");
    printf("user %s login success!index =%d \n", msg->name, *index);
    write(online[*index].fd, &msg_back, sizeof(msg_back));
  }
  // 通知所有客户端,某个用户上线了
  sprintf(buf, "%s online\n", online[*index].name);
  for (i = 0; i < MAX_USER_NUM; i++) {
    if (online[i].fd != -1) {
      write(online[i].fd, buf, strlen(buf));
    }
  }
}
void *func(void *arg) {
  int socket_fd = *((int *)arg);
  char buf[64];
  int len;
  int index = -1;
  struct protocol msg;
  free(arg);
  /*进入聊天*/
  while (1) {
    /*read()函数和recv()函数都可以用于从套接字中读取数据,都会阻塞
     *小于等于0说明未能成功读取任何数据或连接已关闭,因此用户被视为离线
     */
    if ((len = read(socket_fd, &msg, sizeof(msg))) <= 0) {
      printf("%s offline\n", online[index].name);
      del_user_online(index);
      close(socket_fd);
      return;
    }

    switch (msg.cmd) {
    case REGISTE:
      registe(socket_fd, &index, &msg);
      break;
    case LOGIN:
      login(socket_fd, &index, &msg);
      break;
    case BROADCAST:
      broadcast(index, &msg);
      break;
    case PRIVATE:
      private(index, &msg);
      break;
    case ONLINEUSER:
      list_online_user(index);
      break;
    default:
      break;
    }
  }
}
int main(int argc, char *argv[]) {
  int ls_fd, new_fd;
  int addr_len, clientaddr_len;
  struct sockaddr_in my_addr;
  struct sockaddr_in client_addr;
  char buf[64] = {0};
  pthread_t pid;
  int *arg, i, port_number, ret;
  /*判断输入命令行参数数量是否正确*/
  if (argc != 2) {
    fprintf(stderr, "Usage: %s [port_number]>5000\a\n", argv[0]);
    exit(-1);
  }
  /*将第二个参数转换为整数类型并赋值给变量,然后判断输入的端口参数是否小于0*/
  if ((port_number = atoi(argv[1])) < 5000) {
    fprintf(stderr, "Usage: %s [port_number]>5000\a\n", argv[0]);
    exit(-1);
  }
  /*原型:int socket(int domain, int type, int protocol);
   *创建一个网络通信端点(打开一个网络通信),成功则返回一个网络文件描述符;调用失败,则会返回-1
   *domain:AF_INET,协议族名字,代表IPv4互联网协议;如果用IPV6在后面加个6即可
   *type:SOCK_STREAM,这是用于 TCP 协议
   *protocol:0,表示为给定的通信域和套接字类型选择默认协议
   */
  if ((ls_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    perror("socket() fail\n");
    return;
  }
  /*将 server_addr 结构体变量所占用的内存区域全部清零。*/
  memset(&my_addr, 0, sizeof(struct sockaddr_in));
  /*指定套接字服务器的地址信息*/
  /*表示使用 IPv4 地址*/
  my_addr.sin_family = AF_INET;
  /*端口号设置并将主机字节顺序转换为网络字节顺序*/
  my_addr.sin_port = htons(port_number);
  /*使用了宏 INADDR_ANY
   * 来指定监听所有可用的网络接口,从而使得服务器能够接受到所有网络接口上的连接请求*/
  my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  addr_len = sizeof(struct sockaddr_in);
  /*int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
   *将参数 sockfd 指定的套接字与一个地址 addr 进行绑定,成功返回
   *0,失败情况下返回-1 sockfd:网络文件描述符 addr:服务器结构体地址
   *addrlen:addr参数所指向的结构体对应的字节长度
   */
  if (bind(ls_fd, (struct sockaddr *)(&my_addr), addr_len) == -1) {
    perror("bind() fail\n");
    exit(-1);
  }
  /*int listen(int sockfd, int backlog);
   *让服务器进程进入监听状态,监听sockfd描述符并创建一个等待连接队列,若执行成功则返回0,否则返回-1
   *sockfd:要设置为监听状态的套接字描述符
   *backlog:5,指定允许的连接数为
   *5。如果有更多的客户端连接请求到达,它们将被服务器拒绝或忽略。
   */
  if (listen(ls_fd, 5) == -1) {
    fprintf(stderr, "listen error:%s\n\a", strerror(errno));
    return;
  }
  clientaddr_len = sizeof(struct sockaddr_in);
  /*初始化online结构体*/
  for (i = 0; i < 64; i++) {
    online[i].fd = -1;
    online[i].flage = -1;
  }
  while (1) {
    /*int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
     *调用成功返回一个新的
     *socket_fd描述符,用于与客户端通信的。如果发生错误或者连接被中断,则函数返回
     *-1 函数成功返回时,addr 所指的缓冲区将被填充上客户端的地址信息,而 addrlen
     *则会被更新为实际的地址信息长度
     *调用时没有客户端请求连接(等待连接队列中也没有等待连接的请求),
     *accept()会进入阻塞状态,直到客户程序建立连接
     */
    if ((new_fd = accept(ls_fd, (struct sockaddr *)(&client_addr),
                         &clientaddr_len)) == -1) {
      fprintf(stderr, "Accept error:%s\n\a", strerror(errno));
      return;
    }
    /*char *inet_ntoa(struct in_addr in);
     *将32位IP地址转换为点分十进制形式的字符串表示
     */
    printf("Client-ip: %s\tport: %d\t\n", inet_ntoa(client_addr.sin_addr),
           client_addr.sin_port);
    /*用堆空间单独存放new_fd,防止高并发状态覆盖原栈空间的new_fd*/
    arg = malloc(sizeof(int));
    *arg = new_fd;
    /*pthread_create(pthread_t *thread,
                        const pthread_attr_t *attr,
                        void *(*start_routine) (void *),
                        void *arg);
     *创建一个新的线程,返回 0
     表示成功,否则返回一个非零的错误码,表示创建线程失败 *thread:线程标识符pid
     *attr:指定线程的属性,如果传递了 NULL,则表示使用默认属性
     *start_routine:是一个函数指针,用于指定线程启动时要执行的函数
     *arg:传递给该函数的参数
     */
    if ((ret = pthread_create(&pid, NULL, func, (void *)arg)) != 0) {
      perror("pthread_create err");
      return;
    }
  }
  close(new_fd);
  close(ls_fd);
  exit(0);
}

客户端代码

#include "chat.h"
#include <bits/types/timer_t.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int socket_fd, addrlen;
struct sockaddr_in server_addr;

pthread_t pid;

int login_f = -1;

void *func(void *arg) {
  int len;
  char buf[128] = {0};
  struct protocol *msg;
  while (1) {
    if (login_f != 1) {
      continue;
    }
    memset(buf, 0, sizeof(buf));
    if ((len = read(socket_fd, buf, sizeof(buf))) <= 0) {
      close(socket_fd);
      return;
    }
    msg = (struct protocol *)buf;
    /*显示在线用户*/
    if ((msg->state == ONLINEUSER_OK) && (msg->cmd == ONLINEUSER)) {
      printf("%s\t", msg->name);
      continue;
    }
    if ((msg->state == ONLINEUSER_OVER) && (msg->cmd == ONLINEUSER)) {
      printf("\n");
      continue;
    }
    buf[len] = '\0';
    printf("%s\n", buf);
  }
}
/*广播信息*/
void broadcast(int fd) {
  struct protocol msg;
  msg.cmd = BROADCAST;
  printf("say:\n#");
  scanf("%s", msg.data);
  write(fd, &msg, sizeof(msg));
}
/*私聊信息*/
void private(int fd) {
  struct protocol msg, msgback;
  msg.cmd = PRIVATE;
  printf("input name you want to talk:\n\t#");
  scanf("%s", msg.name);
  printf("\n#say:\n");
  scanf("%s", msg.data);
  write(fd, &msg, sizeof(msg));
}
/*显示在线人数*/
void list_online_user(int socket_fd) {
  struct protocol msg;
  msg.cmd = ONLINEUSER;
  write(socket_fd, &msg, sizeof(msg));
  while (1) {
    read(socket_fd, &msg, sizeof(msg));
    if (msg.state != ONLINEUSER_OK) {
      getchar();
      getchar();
      break;
    } else
      printf("%s\r\n", msg.name);
  }
}
/*注册*/
int registe(int fd) {
  struct protocol msg, msgback;
  msg.cmd = REGISTE;
  printf("input your name\n");
  scanf("%s", msg.name);
  printf("input your passwd\n");
  scanf("%s", msg.data);
  write(socket_fd, &msg, sizeof(msg));
  read(socket_fd, &msgback, sizeof(msgback));
  if (msgback.state != OP_OK) {
    printf("\033[0;31m\tName had exist,try again!\n");
    getchar();
    getchar();
    return -1;
  } else {
    printf("\033[0;31m\tRegist success!\n");
    getchar();
    getchar();
    return 0;
  }
}
/*登陆*/
int login(int fd) {
  struct protocol msg, msgback;
  msg.cmd = LOGIN;

  printf("input your name\n");
  scanf("%s", msg.name);
  printf("input your passwd\n");
  scanf("%s", msg.data);
  write(socket_fd, &msg, sizeof(msg));
  read(socket_fd, &msgback, sizeof(msgback));
  if (msgback.state != OP_OK) {
    printf("\033[0;31m\tName had exist,maybe the password is wrong,try "
           "again!\33[0m\n");
    getchar();
    getchar();
    login_f = -1;
    return NAME_PWD_NMATCH;
  } else {
    printf("\033[0;31m\tLogin success!\33[0m\n");
    getchar();
    getchar();
    login_f = 1;
    return OP_OK;
  }
}
/*退出*/
int logout(int fd) {
  close(fd);
  login_f = -1;
}
int main(int argc, char *argv[]) {
  int sel, ret;
  int port_number;
  int min_sel, max_sel;
  struct protocol msg;
  /* 检测参数个数*/
  if (argc != 3) {
    fprintf(stderr, "Usage:%s hostname portnumber\a\n", argv[0]);
    exit(-1);
  }
  /*argv2 存放的是端口号 ,读取该端口,转换成整型变量*/
  if ((port_number = atoi(argv[2])) < 5000) {
    fprintf(stderr, "Usage:%s hostname [portnumber]>5000\a\n", argv[0]);
    exit(-1);
  }
  /*创建一个 套接字*/
  if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    fprintf(stderr, "Socket Error:%s\a\n", strerror(errno));
    return;
  }
  /*填充结构体,ip和port必须是服务器的*/
  memset(&server_addr, 0, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  /*转换为网络字节序*/
  server_addr.sin_port = htons(port_number);
  /*点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址*/
  server_addr.sin_addr.s_addr = inet_addr(argv[1]);
  addrlen = sizeof(struct sockaddr_in);
  if (connect(socket_fd, (struct sockaddr *)(&server_addr), addrlen) == -1) {
    fprintf(stderr, "Connect Error:%s\a\n", strerror(errno));
    return;
  }
  /*创建线程*/
  if ((ret = pthread_create(&pid, NULL, func, NULL)) != 0) {
    perror("pthread_create err");
    return;
  }
  while (1) {
    system("clear");
    if (login_f == -1) {
      printf(
          "\033[0;33m+---------------------------------------------+\033[0m\n");
      printf("\33[0;31m\t 1、 注册 \n\33[0m");
      printf("\33[0;31m\t 2、 登录 \33[0m\n");
    } else if (login_f == 1) {
      printf(
          "\033[0;33m+---------------------------------------------+\033[0m\n");
      printf("\33[0;31m\t 3、 公聊\33[0m\n");
      printf("\33[0;31m\t 4、 私聊\33[0m\n");
      printf("\33[0;31m\t 5、 在线用户\33[0m\n");
    }
    printf("\33[0;31m\t 0、 退出\033[0m\n");
    printf(
        "\033[0;33m+---------------------------------------------+\033[0m\n");
    fflush(stdin);
    scanf("%d", &sel);
    if (sel == 0) {
      break;
    }
    if (login_f == 1) {
      min_sel = 3;
      max_sel = 5;
    } else if (login_f == -1) {
      min_sel = 1;
      max_sel = 2;
    }
    if (sel < min_sel || sel > max_sel) {
      printf("输入的数字不在范围内,请重新输入\n");
      continue;
    }
    switch (sel) {
    case 1:
      registe(socket_fd);
      break;
    case 2:
      login(socket_fd);
      break;
    case 3:
      broadcast(socket_fd);
      break;
    case 4:
      private
      (socket_fd);
      break;
    case 5:
      list_online_user(socket_fd);
      break;
    case 0:
      logout(socket_fd);
      break;
    default:
      break;
    }
    if (sel == 0) {
      exit(0);
    }
  }
}

本项目学习于从0实现基于Linux socket聊天室-多线程服务器模型-1_一口Linux的博客-CSDN博客

从0实现基于Linux socket聊天室-多线程服务器一个很隐晦的错误-2_一口Linux的博客-CSDN博客从0实现基于Linux socket聊天室-多线程服务器一个很隐晦的错误-2_一口Linux的博客-CSDN博客
 从0实现基于Linux socket聊天室-实现聊天室的公聊、私聊功能-4_一口Linux的博客-CSDN博客
 

一口Linux博主实力强悍、经验丰富、知识领域广,笔下文章内容丰富,大家可以点点关注

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值