基于套接字的班级聊天群设计

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

要求:

  利用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\n",j,fd[j]);
 29         printf("username[%d]:%s\n",j,username[j]);
 30         printf("password[%d]:%s\n",j,password[j]);
 31         printf("——————————\n");
 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\n\a",strerror(errno));
 99         exit(1);
100     }
101     else
102     {
103         printf("Socket successful(套接字创建成功)!\n");
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\n\a",strerror(errno));
113         exit(1);
114     }
115     else
116     {
117         printf("Bind successful(绑定成功)!\n");
118     }
119     /* 设置允许连接的最大客户端数 */
120     if(listen(sockfd,BACKLOG)==-1)
121     {
122         fprintf(stderr,"Listen error(打开监听失败):%s\n\a",strerror(errno));
123         exit(1);
124     }
125     else
126     {
127         printf("Listening(监听已打开).....\n");
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(共享内存区创建成功)!\n");
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\n\a",strerror(errno));
183             exit(1);
184         }
185         else
186         {
187             printf("Accept successful(连接分配成功)!\n");
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("\n已连接了客户端%d : %s : %d \n",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,"\n-------欢迎进入聊天室,输入quit退出-------\n");
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,"\n-------欢迎进入聊天室,输入quit退出-------\n");
219             break;
220         }
221         case 1:
222         {
223             login=0;
224             stpcpy(buffer,"\n密码错误,请重新登录!");
225             break;
226         }
227         case 3:
228         {
229             login=0;
230             stpcpy(buffer,"\n聊天室已满!");
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\n",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\n",argv[0]);
 30         exit(1);
 31     }
 32     //使用hostname查询host 名字
 33     if((host=gethostbyname(argv[1]))==NULL)
 34     {
 35         fprintf(stderr,"Gethostname error(获取主机名失败)\n");
 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(套接字创建失败):%s\a\n",strerror(errno));
 45         exit(1);
 46     }
 47     else
 48     {
 49         printf("Socket successful(套接字创建成功)!\n");
 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(连接失败):%s\a\n",strerror(errno));
 60         exit(1);
 61     }
 62     else
 63     {
 64         printf("Connect successful(连接成功)!\n");
 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," \n\t昵称 ->");
 86             strcat(buffer,name);
 87             strcat(buffer,":\n\t\t  ");
 88             memset(buf,0,SIZE);
 89             fgets(buf,SIZE,stdin);
 90             /*对客户端程序进行管理*/
 91             if(strncmp("quit",buf,4)==0)
 92             {
 93                 printf("该客户端下线...\n");
 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,"\n");
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\n",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

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

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

posted @ 2019-06-22 12:02  望三星  阅读(1016)  评论(0编辑  收藏  举报