linux 聊天室

Linux下的shell聊天室:

里程碑0:

1、基于C/S架构的聊天室,分为客户端和服务器。

2、客户端登陆时输入服务器IP、port、 昵称不能重复

里程碑1:

3、可以群发消息或指定接收人

4、admin账号可以踢人

5、新加入用户可以看到聊天历史记录

里程碑2:

5、可以互传文件

6、保存聊天信息

里程碑3:

7、第一次登录时,需要注册。

技术设计:

1、在shell下的图形库:curses

2、多线程:pthread

3、网络:socket

4、消息结构:

<span style="font-size:18px;">struct msg_st
{
    char name[256];
    char to[256];
    char data[4096];
    char cmd[256];
};</span>

其中,关于cmd内容:

//login 登陆

//offline 服务器即将踢人

//user 服务发送的用户列表,数据在data里

//chat 表示这是一条消息,数据在data里

5、服务器用了epoll来监听多个fd

登陆界面:

聊天界面:

以下是源代码(里程碑1):

//client.c
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netdb.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<sys/ioctl.h>
#include<sys/select.h>
#include<pthread.h>
#include<ncurses.h>
#include <sys/epoll.h>

#include "shared_vals.h"
#include"chat_net.h"
#include"chat_ui.h"

char promote[256]= {0};

int main(int argc, char * argv[])
{

    ui_init();

    do
    {
        ui_login(ip,&port,name);
        if(0==net_login(ip,&port,name))
        {
            ui_alert("Login ok!");
            break;
        }
        else
        {
            ui_alert("Login failed!");
        }
    }
    while(1);

    ui_init_wins();

    ui_refresh_wins();
    wprintw(win[1],"IP:%s\nPORT:%d\nNAME:%s",ip,port,name);
    ui_refresh_wins();

//    int i=0;
//    for(i=0; i<100; i++)
//        ui_print_msg("This is a test message!");

    //generate promote as [from] [to]
    sprintf(promote,"[%s]:", name);
    //start recev process

    pthread_t thread;
    int res=pthread_create(&thread,NULL,(void *)net_getmsg,(void*)NULL);
    if(res!=0)
    {
          ui_alert("Error");
          getch();
    }
    while(1)
    {
        ui_promote(promote);

        int c=0;
        struct msg_st msg_client;
        memset((void *)&msg_client,0,sizeof(msg_client));
        ui_getmsg(msg_client.data);
        strcpy(msg_client.name,name);
        strcpy(msg_client.cmd,"chat");
        write(sockfd,&msg_client,sizeof(msg_client));

    }

    net_end();
    ui_end();

    exit(0);
}
//chat_net.h
#ifndef _CHAT_NET_H_
#define _CHAT_NET_H_

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netdb.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<sys/ioctl.h>
#include<sys/select.h>
#include<pthread.h>
#include<ncurses.h>
#include <sys/epoll.h>
#include <time.h>
#include "shared_vals.h"
#include  "chat_ui.h"
int sockfd;
int len;
struct sockaddr_in address;
int result;
char ip[256];
int  port;
char name[256];
int net_login(char *ip,int * port,char *name)
{

    sockfd=socket(AF_INET,SOCK_STREAM,0);
    address.sin_family=AF_INET;
    address.sin_addr.s_addr=inet_addr("127.0.0.1");
    address.sin_port=htons(*port);
    len=sizeof(address);
    result=connect(sockfd,(struct sockaddr *)&address,len);
    if(result==-1)
    {
        printf("ERROR:%s\n",strerror(errno));
        close(sockfd);
        return -1;
    }

    struct msg_st msg;
    memset((void *)&msg,0,sizeof(msg));
    sprintf(msg.cmd,"login");
    strcpy(msg.name,name);
    //  fgets(msg.name,256,stdin);
    //printf("checking name [%s]...\n",msg.name);
    write(sockfd,& msg,sizeof( msg));

    usleep(100000);
    int nread=0;
    ioctl(sockfd,FIONREAD,&nread);
    if(nread!=0)
    {
        read(sockfd,&msg,sizeof(msg));
        if(strcmp("ok",msg.data)==0)
        {
            return 0;//login ok
        }
        else
        {

            // printf("Login failed!\n");
            close(sockfd);
            return -1;//login failed
        }
    }
//   printf("Login failed!\n");
    close(sockfd);
    return -1;//login failed

}

void net_getmsg()
{

    while(1)
    {
        usleep(100000);
        int nread=0;
        ioctl(sockfd,FIONREAD,&nread);
        if(nread!=0)
        {
            struct msg_st msg_read;
            read(sockfd,&msg_read,sizeof(msg_read));
            if(strncmp(msg_read.cmd,"chat",4)==0)
            {
                struct tm *tm_ptr;
                time_t the_time;
                (void)time(&the_time);
                tm_ptr=gmtime(&the_time);
                char buff[256]={0};
                sprintf(buff,"\n[%02d:%02d:%02d]",tm_ptr->tm_hour,tm_ptr->tm_min,tm_ptr->tm_sec);
                strcat(buff,msg_read.name);
                strcat(buff,":");
                ui_print_msg(buff);
                ui_print_msg(msg_read.data);
            }
            else if(strncmp(msg_read.cmd,"user",4)==0)
            {
                 ui_list_users(msg_read.data);
                 ui_input_win_active();
            }
            else if(strncmp(msg_read.cmd,"chat",4)==0)
            {
            }
            else if(strncmp(msg_read.cmd,"chat",4)==0)
            {
            }
            else
            {
            }
            //debug

            //todo

        }
    }
    pthread_exit(NULL);

}

void net_end()
{

    close(sockfd);

}
#endif
//chat_ui.h
#ifndef _CHAT_UI_H_
#define _CHAT_UI_H_
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<curses.h>
#define WIN_MAX  10
#define USR_MAX  20l

//need from other files
//char ip[256];
//int port;
//char name[256];

//char *usr_list[USR_MAX]={"Shenyu",
//                            "Wanglm",
 //                           NULL};

////
WINDOW * win[WIN_MAX ]={0};//max open win = 10
int cur_x,cur_y;

void ui_refresh_wins() //refresh all open wins
{
    int i=0;
    for(i=0;i<WIN_MAX ;i++)
    {
        if(win [i]!=NULL)
        {
            touchwin(win [i]);
            wrefresh(win [i]);
        }
    }
    //the input always active

}

void ui_input_win_active()
{
      touchwin(win [2]);
      wrefresh(win [2]);
}
int ui_init()
{
    initscr();
    refresh();

    start_color();

    init_pair(1,COLOR_BLACK,COLOR_CYAN);
    init_pair(2,COLOR_YELLOW,COLOR_BLUE);
    init_pair(3,COLOR_YELLOW,COLOR_RED);

}
int ui_init_wins()
{
    win[0]=newwin(20,10,0,0);
    win[1]=newwin(20,50,0,10);
    win[2]=newwin(1,60,20,0);

    wbkgd(win[0],COLOR_PAIR(1));
    wbkgd(win[1],COLOR_PAIR(2)|A_BOLD);
    wbkgd(win[2],COLOR_PAIR(3)|A_BOLD);
    scrollok(win[0],TRUE);
    scrollok(win[1],TRUE);

    keypad(win[0],TRUE);
    keypad(win[1],TRUE);
    keypad(win[2],TRUE);
    keypad(stdscr, TRUE);
}
int ui_login(char *ip,int * port,char *name)
{
    win[3]=newwin(10,40,5,10);
    wbkgd(win[3],COLOR_PAIR(3)|A_BOLD);

    wrefresh(win[3]);
    mvwprintw(win[3],3,3,"Server   IP:\n");
    mvwprintw(win[3],4,3,"Server port:\n");
    mvwprintw(win[3],5,3,"User name  :\n");
    wrefresh(win[3]);

    wmove(win[3],3,16);
    wrefresh(win[3]);
    wgetnstr(win[3],ip,100);

    wmove(win[3],4,16);
    wrefresh(win[3]);
    wscanw(win[3],"%d", port);

    wmove(win[3],5,16);
    wrefresh(win[3]);
    wgetnstr(win[3],name,100);
    delwin(win[3]);

}

void ui_list_users(char * data)
{
int i=0;
wclear(win[0]);
 wprintw(win[0],"--users--\n");
 wprintw(win[0],"%s\n",data);
wrefresh(win[0]);

}

void ui_print_msg(char *msg)
{

   wprintw(win[1],"%s\n",msg);
   wrefresh(win[1]);
}

void ui_alert(char *msg)
{
    win[4]=newwin(10,40,5,10);
    wbkgd(win[4],COLOR_PAIR(3)|A_BOLD);
    wrefresh(win[4]);
    mvwprintw(win[4],5,3,"%s\n   Press any key to continue...",msg);
    wrefresh(win[4]);
    getch();
    delwin(win[3]);

}

void ui_end()
{
    endwin();
}
void ui_promote(char *msg)
{
 ui_input_win_active();
 wclear(win[2]);
 wprintw(win[2],"%s",msg);
 wrefresh(win[2]);
}
void ui_getmsg(char * buff)
{
 ui_input_win_active();
 wgetnstr(win[2],buff,4096);

}
//sample programs for UI
//int main(int argc, char * argv[])
//{
//
//
//    ui_init();
//    ui_login(ip,&port,name);
//
//    ui_init_wins();
//    ui_list_users( usr_list );
//    ui_refresh_wins();
//    wprintw(win[1],"IP:%s\nPORT:%d\nNAME:%s",ip,port,name);
//    ui_refresh_wins();
//
//int i=0;
//for(i=0;i<100;i++)
//    ui_print_msg("This is a test message!");
//
//
//    ui_refresh_wins();
//    getch();
//}

#endif // _CHAT_UI_H_
#ifndef _SHARED_VALS_H_
#define _SHARED_VALS_H_

struct msg_st
{
    char name[256];//发送信息人
    char to[256];  //接收人
    char data[4096];//数据
    char cmd[256];//消息的类型
};

//login 登陆
//offline 服务器即将踢人
//user 服务发送的用户列表,数据在data里
//chat 表示这是一条消息,数据在data里

#endif
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netdb.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<sys/ioctl.h>
#include<sys/select.h>
#include<pthread.h>
#include <sys/epoll.h>
#define MAXEVENTS 64

struct msg_st
{
    char name[256];
    char to[256];
    char data[4096];
    char cmd[256];
};
#define MAX_CLIENTS 256
int chat_clients_sets[MAX_CLIENTS]={0};
char *chat_clients_names [MAX_CLIENTS] = {0};
int chat_clients_count=0;

int chat_clients_add(int fd,char * _name)
{
    int i=0;
    for(i=0; i<MAX_CLIENTS; i++)
    {
        if(chat_clients_sets[i]==0)
        {
            chat_clients_sets[i]=fd;

            int len=strlen(_name)+1;
            char *ptr=malloc(len*sizeof(char));
            memset(ptr,0,sizeof(ptr));
            strcpy(ptr,_name);
            *(chat_clients_names+i)=ptr;

            chat_clients_count++;
            return 0;
        }
    }
    return -1;
}
int chat_clients_del(int fd)
{
    int i=0;
    for(i=0;i<MAX_CLIENTS;i++)
    {
        if(chat_clients_sets[i]==fd)
        {
            chat_clients_sets[i]=0;
            free(*(chat_clients_names+i));
            *(chat_clients_names+i)=NULL;
            chat_clients_count--;
            return 0;
        }
    }
    return -1;
}
int chat_user_list(struct msg_st * m)
{
    memset( m,0,sizeof(struct msg_st));
    strcpy(m->cmd,"user");
    int i=0;
    for(i=0;i<MAX_CLIENTS;i++)
    {
        if(*(chat_clients_names+i)!=NULL)
        {
            char buff[4096];
            memset(buff,0,sizeof(buff));

            sprintf(buff,"%s\n",*(chat_clients_names+i));
            strcat(m->data,buff);

        }

    }

}

int chat_clients_sendto_all(struct msg_st * mymsg)
{

    int j=0;
    for (j = 0; j < MAX_CLIENTS; j++)
    {
        if(chat_clients_sets[j]!=0)
        {

            printf("send to %s\n",(*mymsg).name);
          int    s = write (chat_clients_sets[j] , mymsg, sizeof(struct msg_st));
            if (s == -1)
            {
                perror ("write");
                abort ();
            }

        }

    }

}
static int
create_and_bind (char *port)
{
    struct addrinfo hints;
    struct addrinfo *result, *rp;
    int s, sfd;

    memset (&hints, 0, sizeof (struct addrinfo));
    hints.ai_family = AF_UNSPEC;     /* Return IPv4 and IPv6 choices */
    hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
    hints.ai_flags = 0x0001;  /* All interfaces */

    s = getaddrinfo (NULL, port, &hints, &result);
    if (s != 0)
    {
        fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));
        return -1;
    }

    for (rp = result; rp != NULL; rp = rp->ai_next)
    {
        sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sfd == -1)
            continue;
        s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
        if (s == 0)
        {
            /* We managed to bind successfully! */
            break;
        }

        close (sfd);
    }
    if (rp == NULL)
    {
        fprintf (stderr, "Could not bind\n");
        return -1;
    }
    freeaddrinfo (result);
    return sfd;
}

static int
make_socket_non_blocking (int sfd)
{
    int flags, s;

    flags = fcntl (sfd, F_GETFL, 0);
    if (flags == -1)
    {
        perror ("fcntl");
        return -1;
    }

    flags |= O_NONBLOCK;
    s = fcntl (sfd, F_SETFL, flags);
    if (s == -1)
    {
        perror ("fcntl");
        return -1;
    }

    return 0;
}

    int sfd, s;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;

void init(int argc, char * argv[])
{

  if (argc != 2)
    {
        fprintf (stderr, "Usage: %s [port]\n", argv[0]);
        exit (EXIT_FAILURE);
    }

    sfd = create_and_bind (argv[1]);
    if (sfd == -1)
        abort ();

    s = make_socket_non_blocking (sfd);
    if (s == -1)
        abort ();

    s = listen (sfd, SOMAXCONN);
    if (s == -1)
    {
        perror ("listen");
        abort ();
    }

    efd = epoll_create1 (0);
    if (efd == -1)
    {
        perror ("epoll_create");
        abort ();
    }

    event.data.fd = sfd;
    event.events = EPOLLIN | EPOLLET;
    s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
    if (s == -1)
    {
        perror ("epoll_ctl");
        abort ();
    }

    /* Buffer where events are returned */
    events = calloc (MAXEVENTS, sizeof event);
}

void server_accept_clients()
{
 /* We have a notification on the listening socket, which
                   means one or more incoming connections. */
                while (1)
                {
                    struct sockaddr in_addr;
                    socklen_t in_len;
                    int infd;
                    char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

                    in_len = sizeof in_addr;
                    infd = accept (sfd, &in_addr, &in_len);
                    if (infd == -1)
                    {
                        if ((errno == EAGAIN) ||
                                (errno == EWOULDBLOCK))
                        {
                            /* We have processed all incoming
                               connections. */
                            break;
                        }
                        else
                        {
                            perror ("accept");
                            break;
                        }
                    }

                     int s = getnameinfo (&in_addr, in_len,
                                     hbuf, sizeof hbuf,
                                     sbuf, sizeof sbuf,
                                     NI_NUMERICHOST | NI_NUMERICSERV);
                    if (s == 0)
                    {
                        printf("Server: accepted connection on descriptor %d "
                               "(host=%s, port=%s)\n", infd, hbuf, sbuf);
                    }

                    /* Make the incoming socket non-blocking and add it to the
                       list of fds to monitor. */
                    s = make_socket_non_blocking (infd);
                    if (s == -1)
                        abort ();

                    event.data.fd = infd;
                    event.events = EPOLLIN | EPOLLET;
                    s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
                    //add to clients sets
                   // chat_clients_add(infd);
                    if (s == -1)
                    {
                        perror ("epoll_ctl");
                        abort ();
                    }
                }
}
int
main (int argc, char *argv[])
{

    init(argc,argv);

    /* The event loop */
    while (1)
    {
        int n, i;

        n = epoll_wait (efd, events, MAXEVENTS, -1);
        for (i = 0; i < n; i++)
        {
            if ((events[i].events & EPOLLERR) ||
                    (events[i].events & EPOLLHUP) ||
                    (!(events[i].events & EPOLLIN)))
            {
                /* An error has occured on this fd, or the socket is not
                   ready for reading (why were we notified then?) */
                fprintf (stderr, "epoll error\n");
                close (events[i].data.fd);
                continue;
            }

            else if (sfd == events[i].data.fd)
            {
                server_accept_clients();
                continue;
            }
            else
            {
                /* We have data on the fd waiting to be read. Read and
                   display it. We must read whatever data is available
                   completely, as we are running in edge-triggered mode
                   and won't get a notification again for the same
                   data. */
                int done = 0;

                while (1)
                {
                    ssize_t count;
                    //char buf[512]= {0};
                    struct msg_st mymsg;
                    memset((void *)&mymsg,0,sizeof(mymsg));
                    count = read (events[i].data.fd, (void *)&mymsg, sizeof(mymsg));

                    if (count == -1)
                    {
                        /* If errno == EAGAIN, that means we have read all
                           data. So go back to the main loop. */
                        if (errno != EAGAIN)
                        {
                            perror ("read");
                            done = 1;
                        }
                        break;
                    }
                    else if (count == 0)
                    {
                        /* End of file. The remote has closed the
                           connection. */
                        done = 1;
                        break;
                    }
                    /* do a little work here*/

                    /* Write the buffer to standard output */
                    printf("Server: msg from client [%s]-[%s]\n",mymsg.name,mymsg.data);

                    if(strncmp(mymsg.cmd,"login",5)==0)
                    {
                        printf("return login ok!\n");
                        strcpy(mymsg.data,"ok");
                        chat_clients_add(events[i].data.fd,mymsg.name);
                          chat_clients_sendto_all(&mymsg);
                          continue;
                    }

                    strcpy(mymsg.cmd,"chat");
                    chat_clients_sendto_all(&mymsg);
                    usleep(100000);
                    chat_user_list(&mymsg);
                    printf("==user==\n%s\n",mymsg.data);
                    chat_clients_sendto_all(&mymsg);

                }

                if (done)
                {
                    printf ("Server: closed connection on descriptor %d\n",
                            events[i].data.fd);

                    /* Closing the descriptor will make epoll remove it
                       from the set of descriptors which are monitored. */
                    close (events[i].data.fd);
                }
            }
        }
    }

    free (events);

    close (sfd);

    return EXIT_SUCCESS;
}
时间: 2024-10-12 02:17:37

linux 聊天室的相关文章

基于LINUX的多功能聊天室

原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来问我,当然我是做过一遍了,而且为了面试,我将什么strcpy,strlen等最常用的函数都自己实现了一遍,说着,我感觉自己有点挺用功的样子呢! 后来,工作也定下来了,等三方,然后继续帮助我的导师做项目,经过老师的威逼利诱下,我屈服了,又把智能家居系统作为项目,同时也是我的毕业设计,而且功能还要十分完

linux下使用多线程编写的聊天室

自从开始学linux网络编程后就想写个聊天室,一开始原本打算用多进程的方式来写,可是发觉进程间的通信有点麻烦,而且开销也大,后来想用多线程能不能实现呢,于是便去看了一下linux里线程的用法,实际上只需要知道 pthread_create 就差不多了,于是动手开干,用了两天时间,调试的过程挺痛苦的,一开始打算用纯C来撸,便用简单的数组来存储客户端的连接信息,可是运行时出现了一些很奇怪的问题,不知道是不是访问了临界资源,和线程间的互斥有关等等:奇怪的是,当改用STL的set或map时问题就解决了,

基于linux的TCP网络聊天室设计与实现

利用Linux实现基于TCP模式的网络聊天程序 主要完成的两大组成部分为:服务器和客户端. 服务器程序主要负责监听客户端发来的消息. 客户端需要登录到服务器端才可以实现正常的聊天功能.该程序是利用进程以及共享内存来实现群发送消息的. 以下简单分析一下服务器端和客户端两个方面所要完成的任务. 服务器的主要功能如下: 在特定的端口上进行监听,等待客户端的连接. 用户可以配置服务器端的监听端口. 向连接的客户端发送登录成功信息. 向已经连接到服务器的客户端的用户发送系统消息. 使用TCP多线程并发服务

Linux下c++11多线程聊天室

刚看的c++11多线程,写个聊天室试试编译的时候加上 c++11 和 多线程库g++ -Wall -std=c++0x -pthread -o server server.cppserver 和client 都是 q 退出?1. [代码]server.cpp #include <iostream>#include <thread>#include <arpa/inet.h>#include <cstring>#include <vector>#i

Linux网络编程--聊天室客户端程序

聊天室客户端程序 #define _GNU_SOURCE 1 #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <string.h> #in

Python Socket 编程——聊天室演示样例程序

上一篇 我们学习了简单的 Python TCP Socket 编程,通过分别写服务端和client的代码了解主要的 Python Socket 编程模型.本文再通过一个样例来加强一下对 Socket 编程的理解. 聊天室程序需求 我们要实现的是简单的聊天室的样例,就是同意多个人同一时候一起聊天.每一个人发送的消息全部人都能接收到,类似于 QQ 群的功能,而不是点对点的 QQ 好友之间的聊天.例如以下图: 图来自:http://www.ibm.com/developerworks/linux/tu

自写聊天室_LinuxC实现(4)——项目文档

西邮Linux兴趣小组  暑期项目 项目名称:    happychat        项目作者:      楚东方     1. 引言 1.1 项目综述 对项目进行简要介绍,并说明编写此项目的目的. 该项目为聊天室,主要为了实现聊天,文件传输,方便linux环境下的,交流与聊天. 实现功能: 1.好友管理: (1)添加好友 (2)删除好友 2.群管理 (1)创建群 (2)加群 (3)退群 (4)解散群 3.文件传送 实现了上传和下载的断点续传 4.聊天界面分屏 利用光标的移动对输入和屏幕聊天记

php+websocket搭建简易聊天室实践

1.前言 公司游戏里面有个简单的聊天室,了解了之后才知道是node+websocket做的,想想php也来做个简单的聊天室.于是搜集各种资料看文档.找实例自己也写了个简单的聊天室. http连接分为短连接和长连接.短连接一般可以用ajax实现,长连接就是websocket.短连接实现起来比较简单,但是太过于消耗资源.websocket高效不过兼容存在点问题.websocket是html5的资源 如果想要详细了解websocket长连接的原理请看https://www.zhihu.com/ques

多功能聊天室-项目规划实现图

网络编程项目规划 最近在练习一个项目吧,大概给6天的时间来完成它,其中涉及到了数据库的知识,网络编程,linux C等等,大考验的时刻到了!我知道PM们的项目规划图绝对比我的高大上,但是咱也不能没有自信对不?因此,我开始先分析我的项目它有什么功能需求,需求分析完成了,OK,那么项目应该大体上,又是靠什么来实现的呢?这是一个最值得深究的问题!又有人说,那不是得用代码来实现吗?等等,那样你得写到什么时候?因而我们绝对不是先用代码去实现功能,而是把功能提取出来,了解他实现的过程!重要的是什么?嘿嘿,让