zoukankan      html  css  js  c++  java
  • 基于套接字的班级聊天群设计

    嵌入式课程设计做的项目,记录下来。

    要求:

      利用Socket编程设计实现班级聊天群系统,功能主要包括:客户端登陆时,需要手动注册账号;客户端登陆时,已登陆者可以收到某个的登录信息;客户端可以发送群消息,同时除自己外其他登陆者可以收到消息;客户端退出时,会给在线成员退出消息,即提示某人退出;系统可以发送系统消息。

    两种实现方式:线程+信号量,进程+共享内存,这次使用了后者。

    流程图:

    用到的知识点描述:


    1.C语言中常用的字符串处理函数

    strtok(char*src,char*signal)将字符串src按signal字符分隔开

    stpcpy(char*des,char*src)拷贝src字符到des

    strcat(char*des,char*src)将src字符串连接至des

    strcmp(char*des,char*src)比较字符串des和字符串src

     

    2.TCP

    TCP的上一层是应用层,TCP向应用层提供可靠的面向对象的数据流传输服务,TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递。

    通过IP的源/目的可以唯一的区分网络中两个设备的连接,通过socket的源/目的可以唯一的区分网络中两个应用程序的连接。

    三次握手:TCP是面向连接的,就是当计算机双方通信时必需先建立连接,然后进行数据通信,最后拆除连接三个过程。

     

    3.进程

    创建一个新进程的唯一方法就是由某个已存在的进程调用fork或vfork函数,被创建的新进程为子进程,已存在的进程称为父进程。

    fork():用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。

    fork()无参数,是一个单调用双返回函数。

    即某个进程调用此函数后,若创建成功,则此函数在父进程中的返回值是创建的子进程的进程标识号,使父进程利用此进程标识号与子进程取得联系,而在子进程中的返回值为0,否则(创建不成功)返回-1。

    子进程是父进程的一个复制品。

    它从父进程处继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,这些需分配新的内存,而不是与父进程共享内存。而子进程所独有的只有它的进程号、资源使用和计时器等。

     

    4.linux常用的进程间通信机制

    (1)管道(Pipe)及有名管道(named pipe)

    (2)信号(Signal)

    (3)消息队列(Messge Queue)

    (4)共享内存(Shared memory)

    (5)信号量(Semaphore)

    (6)套接字(Socket)

     

    5.共享内存

    共享内存是一种最快的进程间通信方式,因无中间介质,如消息队列、管道等的延迟,进程可以直接读写内存,而不需要任何数据的拷贝。

    共享内存段由一个进程创建,多个进程可以直接读写这一内存区,进行传递消息,而不需进行数据的拷贝,从而大大提高的效率。

    共享内存实现的步骤:

    1)创建共享内存,这里用到的函数是shmget,也就是从内存中获得一段共享内存区域。

    2)映射共享内存,也就是把这段创建的共享内存映射到具体的进程空间中去,这里使用的函数是shmat。

     

    6.套接字

    套接字是一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

    1)套接字定义

    在Linux,网络编程是通过socket接口来进行的。socket是一种特殊的I/O接口,也是一种文件描述符。

    socket是一种常用的进程间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。

    每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。

    socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。

    2)地址结构处理

    struct sockaddr

    {

      unsigned short sa_family; /*地址族*/

      char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号。*/

    };

    struct sockaddr_in

    { short int sa_family; /*地址族*/

      unsigned short int sin_port; /*端口号*/

      struct in_addr sin_addr; /*IP地址*/

      unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/

    };两数据类型等效,可相互转化,sockaddr_in数据类型使用更为方便。在建立socketadd或sockaddr_in后,就可对socket进行适当操作。

    3)地址格式转化

    在IPv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa()。

    而IPv4和IPv6兼容的函数有inet_pton()和inet_ntop()。inet_pton()函数是将点分十进制地址字符串转换为二进制地址。

    inet_ntop()是inet_pton()的反操向作,将二进制地址转换为点分十进制地址字符串。

    4)名字地址转换

    gethostbyname() 根据主机名取得主机信息

    gethostbyaddr() 根据主机地址取得主机信息

    getaddrinfo()还能实现自动识别IPv4地址和IPv6地址

     

    gethostbyname()和gethostbyaddr()都涉及到一个hostent的结构体

    struct hostent

    {

                char *h_name;       ]/*正式主机名*/

                char **h_aliases;      /*主机别名*/

                int h_addrtype;     /*地址类型*/

                int h_length;       /*地址字节长度*/

                char **h_addr_list; /*指向IPv4或IPv6的地址指针数组*/

    }

    7.基于TCP协议socket网络编程

    对服务端(左边):

    (1)socket:创建一个socket套接字;(2)bind:将套接字和服务端主机的IP地址绑定;

    (3)listen:在此套接字上监听;(4)accept:接受客户端发来的连接请求,并创建一个新的套接字,用来和客户端通信;

    (5)recv:在accept分配的端口上接收客户端数据;(6)send:在accept分配的端口上发送数据;

    (7)close:关闭socket。

    对客户端(右边):

    (1)socket:创建一个socket套接字;(2)connect:向服务端发送连接请求;

    (3)send或sendto:向服务端发送数据。(4)recv或recvfrom:从服务端接受数据。

    (5)close:关闭socket。

    系统设计

    Server(服务器)

    ①创建并映射共享内存区shmget()、shmat()

    ②创建服务器套接字get_socket()、bind()、listen()

    ③接收客户端连接请求accept()

    ④接收用户名密码recv()

    ⑤判断登录状态judge()

    ⑥创建子进程反馈登录信息,并将登录信息发送给在线用户

    ⑦创建子进程收发信息fork()

    Client(客户端)

    Client(5个参数)

    ①通过参数0指向运行程序的路径

                  参数1获取主机号

                  参数2获取端口号

                  参数3、4获取用户名密码  

                  struct   sockaddr_in

    ②创建套接字socket()

    ③发起连接请求connect()

    ④创建父子进程:

    父进程从标准输入获取信息、发送客户信息fgets()、send();子进程接收服务端信息recv()

    控制流程

    选择局域网内一台主机作为服务端,在其终端内运行编译好的服务端程序

    ./server,若显示监听已打开,则说明服务端开启成功,局域网内其余主机作为客户端在终端内运行编译好的客户端程序,具体用法为,若显示监听已打开,则说明服务端开启成功,局域网内其余主机作为客户端在终端内运行编译好的客户端程序,具体用法为:./client 主机ip地址 端口号  用户名 密码。在最大人数允许范围内的客户机即可进入聊天室。

     

    共享内存同步过程(核心)

    1.

    2.

     3.

    源代码

    server.c

      1 #include<stdio.h>
      2 #include<stdlib.h>
      3 #include<sys/types.h>
      4 #include<sys/stat.h>
      5 #include<netinet/in.h>
      6 #include<sys/socket.h>
      7 #include<string.h>
      8 #include<unistd.h>
      9 #include<signal.h>
     10 #include<sys/ipc.h>
     11 #include<errno.h>
     12 #include<sys/shm.h>
     13 #include<time.h>
     14 #include<pthread.h>
     15 #define PORT 4395
     16 #define SIZE 1024
     17 #define SIZE_SHMADD 2048
     18 #define BACKLOG 3
     19 int sockfd;
     20 int fd[BACKLOG];
     21 
     22 //显示当前数组
     23 void prt(char username[][30],char password[][30])
     24 {
     25     int j;
     26     for(j=0; j<BACKLOG; j++)
     27     {
     28         printf("fd[%d]:%d
    ",j,fd[j]);
     29         printf("username[%d]:%s
    ",j,username[j]);
     30         printf("password[%d]:%s
    ",j,password[j]);
     31         printf("——————————
    ");
     32     }
     33 }
     34 
     35 //判断fd[]是否有空闲
     36 int judgefree()
     37 {
     38     int j;
     39     for(j=0; j<BACKLOG; j++)
     40     {
     41         if(fd[j]==0)
     42             return j;
     43     }
     44     return -1;
     45 }
     46 
     47 //判断是否是老用户? j:0
     48 int judgeuser(char* name,char username[][30])
     49 {
     50     int j;
     51     for(j=0; j<BACKLOG; j++)
     52     {
     53         if(name!="" && strcmp(name,username[j])==0)
     54             return j;
     55     }
     56     return -1;
     57 }
     58 
     59 //判断密码是否正确? 1:0
     60 int judgepassword(int n,char* psd,char password[][30])
     61 {
     62     if(psd!="" && strcmp(psd,password[n])==0)
     63         return 1;
     64     return 0;
     65 }
     66 
     67 //判断用户登录状态
     68 int judge(char* name,char * psd,char username[][30],char password[][30])
     69 {
     70     int i=judgeuser(name,username);
     71     int j=judgefree();
     72     if(i >= 0)
     73     {
     74         if(judgepassword(i,psd,password))
     75         {
     76             return 0;//老用户且密码正确
     77         }
     78         else
     79             return 1;//密码错误
     80     }
     81     else
     82     {
     83         if(j>=0)
     84         {
     85             return 2;//聊天室有空位
     86         }
     87         else
     88             return 3;//聊天室已满
     89     }
     90 
     91 }
     92 //套接字描述符
     93 int get_sockfd()
     94 {
     95     struct sockaddr_in server_addr;
     96     if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
     97     {
     98         fprintf(stderr,"Socket error(套接字创建错误):%s
    a",strerror(errno));
     99         exit(1);
    100     }
    101     else
    102     {
    103         printf("Socket successful(套接字创建成功)!
    ");
    104     }
    105     bzero(&server_addr,sizeof(struct sockaddr_in));
    106     server_addr.sin_family=AF_INET;
    107     server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    108     server_addr.sin_port=htons(PORT);
    109     /*绑定服务器的ip和服务器端口号*/
    110     if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
    111     {
    112         fprintf(stderr,"Bind error(绑定失败):%s
    a",strerror(errno));
    113         exit(1);
    114     }
    115     else
    116     {
    117         printf("Bind successful(绑定成功)!
    ");
    118     }
    119     /* 设置允许连接的最大客户端数 */
    120     if(listen(sockfd,BACKLOG)==-1)
    121     {
    122         fprintf(stderr,"Listen error(打开监听失败):%s
    a",strerror(errno));
    123         exit(1);
    124     }
    125     else
    126     {
    127         printf("Listening(监听已打开).....
    ");
    128     }
    129     return sockfd;
    130 }
    131 
    132 /*创建共享存储区*/
    133 int shmid_create()
    134 {
    135     int shmid;
    136     if((shmid = shmget(IPC_PRIVATE,SIZE_SHMADD,0777)) < 0)
    137     {
    138         perror("shmid error(共享内存区创建失败)!");
    139         exit(1);
    140     }
    141     else
    142         printf("shmid success(共享内存区创建成功)!
    ");
    143     return shmid;
    144 }
    145 
    146 int main(int argc, char *argv[])
    147 {
    148     int shmid;
    149     char *shmadd;
    150     /***********共享内存**************/
    151     shmid = shmid_create();
    152     //映射共享内存
    153     shmadd = shmat(shmid, 0, 0);
    154 
    155     char username[BACKLOG][30]= {"","",""};
    156     char password[BACKLOG][30]= {"","",""};
    157     
    158     int mark=0;
    159     char usermsg[SIZE];
    160     char shmadd_buffer[SIZE_SHMADD];
    161     char buffer[SIZE];
    162 
    163     struct sockaddr_in client_addr;
    164     int new_fd;
    165     int i;
    166     char* name="";
    167     char* psd="";
    168     int login=0;
    169     int sin_size;
    170     pid_t ppid,pid;
    171     //创建套接字描述符
    172     int sockfd = get_sockfd();
    173     
    174     //循环接收客户端
    175     
    176     while(1)//服务器阻塞,直到客户程序建立连接
    177     {
    178         
    179         sin_size=sizeof(struct sockaddr_in);
    180         if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)
    181         {
    182             fprintf(stderr,"Accept error(连接分配失败):%s
    a",strerror(errno));
    183             exit(1);
    184         }
    185         else
    186         {
    187             printf("Accept successful(连接分配成功)!
    ");
    188         }
    189 
    190         memset(usermsg,0,SIZE);
    191         memset(buffer,0,SIZE);
    192         recv(new_fd,buffer,SIZE,0);
    193         //截取用户名和密码
    194         strcpy(usermsg,buffer);
    195         name=strtok(usermsg,"&");
    196         psd=strtok(NULL,"&");
    197         mark=judgefree(fd);
    198         printf("
    已连接了客户端%d : %s : %d 
    ",mark,inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    199         memset(buffer,0,SIZE);
    200         //判断用户登录状态
    201         switch(judge(name,psd,username,password))
    202         {
    203         case 0:
    204         {
    205             i=judgeuser(name,username);
    206             fd[i]=new_fd;
    207             login=1;
    208             strcpy(buffer,"
    -------欢迎进入聊天室,输入quit退出-------
    ");
    209             break;
    210         }
    211         case 2:
    212         {
    213             mark=judgefree(fd);
    214             fd[mark] = new_fd;
    215             strcpy(username[mark],name);
    216             strcpy(password[mark],psd);
    217             login=1;
    218             strcpy(buffer,"
    -------欢迎进入聊天室,输入quit退出-------
    ");
    219             break;
    220         }
    221         case 1:
    222         {
    223             login=0;
    224             stpcpy(buffer,"
    密码错误,请重新登录!");
    225             break;
    226         }
    227         case 3:
    228         {
    229             login=0;
    230             stpcpy(buffer,"
    聊天室已满!");
    231             break;
    232         }
    233         }
    234         ppid=fork();
    235         if(ppid==0)
    236         {
    237             send(new_fd,buffer,strlen(buffer),0);
    238             if(login==1)
    239             {
    240                 prt(username,password);
    241                 //将加入的新客户发送给所有在线的客户端
    242                 memset(buffer,0,SIZE);
    243                 stpcpy(buffer,name);
    244                 strcat(buffer," 进入了聊天室....");
    245                 for(i=0; i<BACKLOG; i++)
    246                 {
    247                     if(fd[i]!=-1)
    248                     {
    249                         send(fd[i],buffer,strlen(buffer),0);
    250                     }
    251                 }
    252                 //创建子进程进行读写操作/
    253                 pid = fork();//fork()创建时,复制父进程变量状态
    254                 while(1)
    255                 {
    256                     if(pid > 0)
    257                     {
    258                         //父进程用于接收信息/
    259                         memset(buffer,0,SIZE);
    260                         if((recv(new_fd,buffer,SIZE,0)) <= 0)
    261                         {
    262                             close(new_fd);
    263                             exit(1);
    264                         }
    265                         strncpy(shmadd, buffer, SIZE_SHMADD);//将缓存区的客户端信息放入共享内存里
    266                         printf(" %s
    ",buffer);
    267                     }
    268                     if(pid == 0)
    269                     {
    270                         //子进程用于发送信息/
    271                         sleep(1);//先执行父进程
    272                         if(strcmp(shmadd_buffer,shmadd) != 0)
    273                         {
    274                             strcpy(shmadd_buffer,shmadd);
    275                             if(new_fd  > 0)
    276                             {
    277                                 if(send(new_fd,shmadd,strlen(shmadd),0) == -1)
    278                                 {
    279                                     perror("error send(发送失败)!");
    280                                 }
    281                                 strcpy(shmadd,shmadd_buffer);
    282                             }
    283                         }
    284                     }
    285 
    286                 }
    287             }
    288         }
    289     }
    290     free(buffer);
    291     close(new_fd);
    292     close(sockfd);
    293     return 0;
    294 }

    client.c

      1 #include<stdio.h>
      2 #include<netinet/in.h>
      3 #include<sys/socket.h>
      4 #include<sys/types.h>
      5 #include<string.h>
      6 #include<stdlib.h>
      7 #include<netdb.h>
      8 #include<unistd.h>
      9 #include<signal.h>
     10 #include<errno.h>
     11 #include<time.h>
     12 #define SIZE 1024
     13 
     14 int main(int argc, char *argv[])
     15 {
     16     pid_t pid;
     17     int sockfd,confd;
     18     char buffer[SIZE],buf[SIZE];
     19     struct sockaddr_in server_addr;
     20     struct sockaddr_in client_addr;
     21     struct hostent* host;
     22     short port;
     23     char* name;
     24     char* password;
     25     int n=1;
     26     //5个参数
     27     if(argc!=5)
     28     {
     29         fprintf(stderr,"用法:%s 主机名 端口号 用户名 密码 a
    ",argv[0]);
     30         exit(1);
     31     }
     32     //使用hostname查询host 名字
     33     if((host=gethostbyname(argv[1]))==NULL)
     34     {
     35         fprintf(stderr,"Gethostname error(获取主机名失败)
    ");
     36         exit(1);
     37     }
     38     port=atoi(argv[2]);
     39     name=argv[3];
     40     password=argv[4];
     41     /*客户程序开始建立 sockfd描述符 */
     42     if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
     43     {
     44         fprintf(stderr,"Socket Error(套接字创建失败):%sa
    ",strerror(errno));
     45         exit(1);
     46     }
     47     else
     48     {
     49         printf("Socket successful(套接字创建成功)!
    ");
     50     }
     51     /*客户程序填充服务端的资料 */
     52     bzero(&server_addr,sizeof(server_addr)); // 初始化,置0
     53     server_addr.sin_family=AF_INET;          // IPV4
     54     server_addr.sin_port=htons(port);  // (将本机器上的short数据转化为网络上的short数据)端口号
     55     server_addr.sin_addr=*((struct in_addr *)host->h_addr); // IP地址
     56     /* 客户程序发起连接请求 */
     57     if(confd=connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
     58     {
     59         fprintf(stderr,"Connect Error(连接失败):%sa
    ",strerror(errno));
     60         exit(1);
     61     }
     62     else
     63     {
     64         printf("Connect successful(连接成功)!
    ");
     65     }
     66     /*将客户端的名字、密码发送到服务器端*/
     67     memset(buffer,0,SIZE);
     68     strcat(buffer,name);
     69     strcat(buffer,"&");
     70     strcat(buffer,password);
     71     send(sockfd,buffer,SIZE,0);
     72     /*创建子进程,进行读写操作*/
     73     pid = fork();//创建子进程
     74     while(1)
     75     {
     76         /*父进程用于发送信息*/
     77         if(pid > 0)
     78         {
     79             memset(buffer,0,SIZE);
     80             /*时间函数*/
     81             time_t timep=time(NULL);
     82             struct tm *p=localtime(&timep);
     83             strftime(buffer, sizeof(buffer), "%Y/%m/%d %H:%M:%S", p);
     84             /*输出时间和客户端的名字*/
     85             strcat(buffer," 
    	昵称 ->");
     86             strcat(buffer,name);
     87             strcat(buffer,":
    		  ");
     88             memset(buf,0,SIZE);
     89             fgets(buf,SIZE,stdin);
     90             /*对客户端程序进行管理*/
     91             if(strncmp("quit",buf,4)==0)
     92             {
     93                 printf("该客户端下线...
    ");
     94                 strcat(buffer,"退出聊天室!");
     95                 if((send(sockfd,buffer,SIZE,0)) <= 0)
     96                 {
     97                     perror("error send(发送失败)!");
     98                 }
     99                 close(sockfd);
    100                 sockfd = -1;
    101                 exit(0);
    102             }
    103             else
    104             {
    105                 strncat(buffer,buf,strlen(buf)-1);
    106                 strcat(buffer,"
    ");
    107                 if(strlen(buffer) > 38+strlen(name))//防止发空消息
    108                     n=send(sockfd,buffer,SIZE,0);
    109                 if(n<= 0)
    110                     perror("error send(发送失败)!");
    111             }
    112         }
    113         else if(pid == 0)
    114         {
    115             /*子进程用于接收信息*/
    116             memset(buffer,0,SIZE);
    117             if(sockfd > 0)
    118             {
    119                 if((recv(sockfd,buffer,SIZE,0)) <= 0)
    120                 {
    121                     close(sockfd);
    122                     exit(1);
    123                 }
    124                 printf("%s
    ",buffer);
    125             }
    126         }
    127     }
    128     close(sockfd);
    129     return 0;
    130 }

    主要函数说明:

    • void prt(char username[][30],char password[][30]) //显示当前数组
    • int judgefree() //判断fd[]是否有空闲
    • int judgeuser(char* name,char username[][30]) //判断是否是老用户? j:0
    • int judgepassword(int n,char* psd,char password[][30] //判断密码是否正确? 1:0
    • int judge(char* name,char * psd,char username[][30],char password[][30]) //判断用户登录状态
    • int get_sockfd() //套接字描述符
    • int shmid_create() /*创建共享存储区*/
    • strtok(char*src,char*signal)将字符串src按signal字符分隔开
    • stpcpy(char*des,char*src)拷贝src字符到des
    • strcat(char*des,char*src)
    • memset(char*buf,int start,int size)将src字符串连接至des后,从start处,清空buf里的size个大小
    • send(int fd,char*buf,strlen,0)将buf内的信息,发送至fd
    • fork()创建进程
    • recv(int fd,char*buf,size,0)将来自fd的信息接收,放于buf内
    • socket(AF_INET,SOCK_STREAM,0)通过IPV4协议簇,套接字字节流创建套接字
    • gethostbyname(char*a)通过a字符串获取主机名
    • bzero (&addr,size)将addr所在地址,size大小置0初始化

     运行结果

    服务端程序启动,连接了客户机Tom,并将其信息保存

    Tom启动客户端程序,当前聊天室有空位,则Tom进入聊天室

    Pom启动客户端程序,当前聊天室有空位,则Pom进入聊天室

    Tom和Pom可在聊天室互发消息

    Jhon启动客户端程序,当前聊天室有空位,则Jhon进入聊天室

    Mary启动客户端程序,当前聊天室无空位,则Mary无法进入聊天室

    Tom输入quit下线后再上线,密码正确则进入聊天室

    此时Tom对应的套接字描述符fd[0]已经变为新的8,说明Tom是再次上线的

    存在BUG

      客户端用户下线后,以相同账号再次登录,收消息时会出现收到两条重复消息的情况。

      用户端下线只是用户单方面下线,服务端仍保留着该用户的套接字描述符,从服务端的角度来看该用户端仍然在线。

  • 相关阅读:
    JavaScript技巧
    函数
    windows实现应用双开
    vue组件中name属性有啥作用
    文本超出长度后多余部分显示省略号
    el-tree控件动态获取数据赋值给treeData渲染问题:render-after-expand属性
    elementUI弹框dialog的打开和关闭
    自然语言处理工具之gensim / 预训练模型 word2vec doc2vec
    Linux 根目录空间不足解决方法
    文本挖掘预处理之分词 / 向量化 / TF-IDF / Hash trick 附代码 Demo
  • 原文地址:https://www.cnblogs.com/kaml8/p/11068370.html
Copyright © 2011-2022 走看看