基于套接字的班级聊天群设计
嵌入式课程设计做的项目,记录下来。
要求:
利用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
客户端用户下线后,以相同账号再次登录,收消息时会出现收到两条重复消息的情况。
用户端下线只是用户单方面下线,服务端仍保留着该用户的套接字描述符,从服务端的角度来看该用户端仍然在线。