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