经过令国鸡冻的APEC会之后,北京的冬天终于不冷了,有暖气的日子就是倍儿爽呀~~洗完热水澡,舒服的躺在床上欢乐地敲打着键盘,是件多么幸福的事呀,好了,抒发情感后,正题继续。
上节中已经初步学习了UDP的编程,这次主要是进一步加深对UDP的认识,用它来实现一个简易的聊天室程序,下面首先来看一下该程序的总的逻辑架构图:
下面来将其进行分解:
以上就是聊天程序所涉及的一些消息交互的过程,在正式开始代码前,先来看一下该程序的最后效果,对其有一个更加直观的感觉:
接下来再来登录一个用户,这时还是登录aa,用有什么效果呢?
下面给用户发送消息:
接下来客户端退出:
以上就是聊天程序的效果,下面则正式进入代码的阶段,重在分析其流程:
先看一下代码结构:
其中在上面看到了很多状态消息,都利用宏定义在pub.h头文件中:
接下来定义的一些消息结构,也定义在头文件中:
【说明】:关于这里用到的c++知识可以完全理解既可,稍有一点上层编程语言的都很容易理解,例如java,实际上我也还没学过c++的内容,不过将来会扎实地学习它的,这里只是为了实验需要,重在实验的理解。
下面贴出头文件的具体代码:
pub.h:
#ifndef _PUB_H_ #define _PUB_H_ #include <list> #include <algorithm> using namespace std; // C2S #define C2S_LOGIN 0x01 #define C2S_LOGOUT 0x02 #define C2S_ONLINE_USER 0x03 #define MSG_LEN 512 // S2C #define S2C_LOGIN_OK 0x01 #define S2C_ALREADY_LOGINED 0x02 #define S2C_SOMEONE_LOGIN 0x03 #define S2C_SOMEONE_LOGOUT 0x04 #define S2C_ONLINE_USER 0x05 // C2C #define C2C_CHAT 0x06 typedef struct message { int cmd; char body[MSG_LEN]; } MESSAGE; typedef struct user_info { char username[16]; unsigned int ip; unsigned short port; } USER_INFO; typedef struct chat_msg { char username[16]; char msg[100]; }CHAT_MSG; typedef list<USER_INFO> USER_LIST; #endif /* _PUB_H_ */
接着开始分析下服务端的代码:
其main函数代码就不做过多解释了,上节UDP编程中已经详细提到过,下面先贴出来:
下面来具体分析该函数:
对应于逻辑图:
下面各个消息处理进行一一分解:
登录do_login:
void do_login(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr) { //从客户端信息中来初使化user结构体 USER_INFO user; strcpy(user.username, msg.body); user.ip = cliaddr->sin_addr.s_addr; user.port = cliaddr->sin_port; }
接下来判断用户是否已经登录过:
void do_login(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr) { //从客户端信息中来初使化user结构体 USER_INFO user; strcpy(user.username, msg.body); user.ip = cliaddr->sin_addr.s_addr; user.port = cliaddr->sin_port; /* 查找用户 */ USER_LIST::iterator it; for (it=client_list.begin(); it != client_list.end(); ++it) { if (strcmp(it->username,msg.body) == 0) { break; } } if (it == client_list.end()) /* 没找到用户 */ { printf("has a user login : %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); client_list.push_back(user); } else /* 找到用户 */ { printf("user %s has already logined ", msg.body); } }
如果是没有登录过,那就是登录成功了,接下来会进行一系列处理,由于便于理解流程,所以下面说明时会对照着客户端的代码:
void do_login(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr) { //从客户端信息中来初使化user结构体 USER_INFO user; strcpy(user.username, msg.body); user.ip = cliaddr->sin_addr.s_addr; user.port = cliaddr->sin_port; /* 查找用户 */ USER_LIST::iterator it; for (it=client_list.begin(); it != client_list.end(); ++it) { if (strcmp(it->username,msg.body) == 0) { break; } } if (it == client_list.end()) /* 没找到用户 */ { printf("has a user login : %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); client_list.push_back(user);//将新的用户插入到集合中 // 登录成功应答 MESSAGE reply_msg; memset(&reply_msg, 0, sizeof(reply_msg)); reply_msg.cmd = htonl(S2C_LOGIN_OK); sendto(sock, &reply_msg, sizeof(msg), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); } else /* 找到用户 */ { printf("user %s has already logined ", msg.body); } }
这时看一下客户端的代码,登录成功应答时客户端是怎么处理的:
void chat_cli(int sock) { struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); struct sockaddr_in peeraddr; socklen_t peerlen; MESSAGE msg; while (1) { //输入用户名 memset(username,0,sizeof(username)); printf("please inpt your name:"); fflush(stdout); scanf("%s", username); //准备向服务端发送登录请求 memset(&msg, 0, sizeof(msg)); msg.cmd = htonl(C2S_LOGIN); strcpy(msg.body, username); //发送登录请求给服务端 sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); memset(&msg, 0, sizeof(msg)); //接收服务端的消息,其中就是登录请求的应答信息 recvfrom(sock, &msg, sizeof(msg), 0, NULL, NULL); int cmd = ntohl(msg.cmd); if (cmd == S2C_ALREADY_LOGINED)//证明用户已经登录过 printf("user %s already logined server, please use another username ", username); else if (cmd == S2C_LOGIN_OK) {//证明用户已经成功登录了 printf("user %s has logined server ", username); break; } } }
接着服务端向客户端发送在线人数及列表:
chatsrv.cpp:
void do_login(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr) { //从客户端信息中来初使化user结构体 USER_INFO user; strcpy(user.username, msg.body); user.ip = cliaddr->sin_addr.s_addr; user.port = cliaddr->sin_port; /* 查找用户 */ USER_LIST::iterator it; for (it=client_list.begin(); it != client_list.end(); ++it) { if (strcmp(it->username,msg.body) == 0) { break; } } if (it == client_list.end()) /* 没找到用户 */ { printf("has a user login : %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); client_list.push_back(user);//将新的用户插入到集合中 // 登录成功应答 MESSAGE reply_msg; memset(&reply_msg, 0, sizeof(reply_msg)); reply_msg.cmd = htonl(S2C_LOGIN_OK); sendto(sock, &reply_msg, sizeof(msg), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); int count = htonl((int)client_list.size()); // 发送在线人数 sendto(sock, &count, sizeof(int), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); printf("sending user list information to: %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); // 发送在线列表 for (it=client_list.begin(); it != client_list.end(); ++it) { sendto(sock, &*it/* *it表示USER_INFO */, sizeof(USER_INFO), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); } } else /* 找到用户 */ { printf("user %s has already logined ", msg.body); } }
客户端收到在线列表的处理代码:
chatcli.cpp:
void chat_cli(int sock) { struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); struct sockaddr_in peeraddr; socklen_t peerlen; MESSAGE msg; while (1) { //输入用户名 memset(username,0,sizeof(username)); printf("please inpt your name:"); fflush(stdout); scanf("%s", username); //准备向服务端发送登录请求 memset(&msg, 0, sizeof(msg)); msg.cmd = htonl(C2S_LOGIN); strcpy(msg.body, username); //发送登录请求给服务端 sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); memset(&msg, 0, sizeof(msg)); //接收服务端的消息,其中就是登录请求的应答信息 recvfrom(sock, &msg, sizeof(msg), 0, NULL, NULL); int cmd = ntohl(msg.cmd); if (cmd == S2C_ALREADY_LOGINED)//证明用户已经登录过 printf("user %s already logined server, please use another username ", username); else if (cmd == S2C_LOGIN_OK) {//证明用户已经成功登录了 printf("user %s has logined server ", username); break; } } int count; recvfrom(sock, &count, sizeof(int), 0, NULL, NULL); int n = ntohl(count); printf("has %d users logined server ", n); for (int i=0; i<n; i++) { USER_INFO user; recvfrom(sock, &user, sizeof(USER_INFO), 0, NULL, NULL); client_list.push_back(user);//每接收到一个用户,则插入到聊天成员列表中 in_addr tmp; tmp.s_addr = user.ip; printf("%d %s <-> %s:%d ", i, user.username, inet_ntoa(tmp), ntohs(user.port)); } }
下面则向其它用户通知有新用户登录:
void do_login(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr) { //从客户端信息中来初使化user结构体 USER_INFO user; strcpy(user.username, msg.body); user.ip = cliaddr->sin_addr.s_addr; user.port = cliaddr->sin_port; /* 查找用户 */ USER_LIST::iterator it; for (it=client_list.begin(); it != client_list.end(); ++it) { if (strcmp(it->username,msg.body) == 0) { break; } } if (it == client_list.end()) /* 没找到用户 */ { printf("has a user login : %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); client_list.push_back(user);//将新的用户插入到集合中 // 登录成功应答 MESSAGE reply_msg; memset(&reply_msg, 0, sizeof(reply_msg)); reply_msg.cmd = htonl(S2C_LOGIN_OK); sendto(sock, &reply_msg, sizeof(msg), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); int count = htonl((int)client_list.size()); // 发送在线人数 sendto(sock, &count, sizeof(int), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); printf("sending user list information to: %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); // 发送在线列表 for (it=client_list.begin(); it != client_list.end(); ++it) { sendto(sock, &*it, sizeof(USER_INFO), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); } // 向其他用户通知有新用户登录 for (it=client_list.begin(); it != client_list.end(); ++it) { if (strcmp(it->username,msg.body) == 0) continue; struct sockaddr_in peeraddr; memset(&peeraddr, 0, sizeof(peeraddr)); peeraddr.sin_family = AF_INET; peeraddr.sin_port = it->port; peeraddr.sin_addr.s_addr = it->ip; msg.cmd = htonl(S2C_SOMEONE_LOGIN); memcpy(msg.body, &user, sizeof(user)); if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&peeraddr, sizeof(peeraddr)) < 0) ERR_EXIT("sendto"); } } else /* 找到用户 */ { printf("user %s has already logined ", msg.body); } }
如果发现该用户已经登录了,则给出已登录的提示:
void do_login(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr) { //从客户端信息中来初使化user结构体 USER_INFO user; strcpy(user.username, msg.body); user.ip = cliaddr->sin_addr.s_addr; user.port = cliaddr->sin_port; /* 查找用户 */ USER_LIST::iterator it; for (it=client_list.begin(); it != client_list.end(); ++it) { if (strcmp(it->username,msg.body) == 0) { break; } } if (it == client_list.end()) /* 没找到用户 */ { printf("has a user login : %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); client_list.push_back(user);//将新的用户插入到集合中 // 登录成功应答 MESSAGE reply_msg; memset(&reply_msg, 0, sizeof(reply_msg)); reply_msg.cmd = htonl(S2C_LOGIN_OK); sendto(sock, &reply_msg, sizeof(msg), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); int count = htonl((int)client_list.size()); // 发送在线人数 sendto(sock, &count, sizeof(int), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); printf("sending user list information to: %s <-> %s:%d ", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port)); // 发送在线列表 for (it=client_list.begin(); it != client_list.end(); ++it) { sendto(sock, &*it, sizeof(USER_INFO), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); } // 向其他用户通知有新用户登录 for (it=client_list.begin(); it != client_list.end(); ++it) { if (strcmp(it->username,msg.body) == 0) continue; struct sockaddr_in peeraddr; memset(&peeraddr, 0, sizeof(peeraddr)); peeraddr.sin_family = AF_INET; peeraddr.sin_port = it->port; peeraddr.sin_addr.s_addr = it->ip; msg.cmd = htonl(S2C_SOMEONE_LOGIN); memcpy(msg.body, &user, sizeof(user)); if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&peeraddr, sizeof(peeraddr)) < 0) ERR_EXIT("sendto"); } } else /* 找到用户 */ { printf("user %s has already logined ", msg.body); MESSAGE reply_msg; memset(&reply_msg, 0, sizeof(reply_msg)); reply_msg.cmd = htonl(S2C_ALREADY_LOGINED); sendto(sock, &reply_msg, sizeof(reply_msg), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in)); } }
接下来,回到客户端这边来,当登录成功之后,会列出该客户端能用到的命令:
chatcli.cpp:
void chat_cli(int sock) { struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); struct sockaddr_in peeraddr; socklen_t peerlen; MESSAGE msg; while (1) { //输入用户名 memset(username,0,sizeof(username)); printf("please inpt your name:"); fflush(stdout); scanf("%s", username); //准备向服务端发送登录请求 memset(&msg, 0, sizeof(msg)); msg.cmd = htonl(C2S_LOGIN); strcpy(msg.body, username); //发送登录请求给服务端 sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); memset(&msg, 0, sizeof(msg)); //接收服务端的消息,其中就是登录请求的应答信息 recvfrom(sock, &msg, sizeof(msg), 0, NULL, NULL); int cmd = ntohl(msg.cmd); if (cmd == S2C_ALREADY_LOGINED)//证明用户已经登录过 printf("user %s already logined server, please use another username ", username); else if (cmd == S2C_LOGIN_OK) {//证明用户已经成功登录了 printf("user %s has logined server ", username); break; } } int count; recvfrom(sock, &count, sizeof(int), 0, NULL, NULL); int n = ntohl(count); printf("has %d users logined server ", n); for (int i=0; i<n; i++) { USER_INFO user; recvfrom(sock, &user, sizeof(USER_INFO), 0, NULL, NULL); client_list.push_back(user); in_addr tmp; tmp.s_addr = user.ip; printf("%d %s <-> %s:%d ", i, user.username, inet_ntoa(tmp), ntohs(user.port)); } printf(" Commands are: "); printf("send username msg "); printf("list "); printf("exit "); printf(" "); }
接下来用I/O复用模型select函数,来并发处理I/O套接字,因为既有可能产生键盘套接字,也有sock,所以需要用I/O复用模型,如下:
chatcli.cpp:
void chat_cli(int sock) { struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); struct sockaddr_in peeraddr; socklen_t peerlen; MESSAGE msg; while (1) { //输入用户名 memset(username,0,sizeof(username)); printf("please inpt your name:"); fflush(stdout); scanf("%s", username); //准备向服务端发送登录请求 memset(&msg, 0, sizeof(msg)); msg.cmd = htonl(C2S_LOGIN); strcpy(msg.body, username); //发送登录请求给服务端 sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); memset(&msg, 0, sizeof(msg)); //接收服务端的消息,其中就是登录请求的应答信息 recvfrom(sock, &msg, sizeof(msg), 0, NULL, NULL); int cmd = ntohl(msg.cmd); if (cmd == S2C_ALREADY_LOGINED)//证明用户已经登录过 printf("user %s already logined server, please use another username ", username); else if (cmd == S2C_LOGIN_OK) {//证明用户已经成功登录了 printf("user %s has logined server ", username); break; } } int count; recvfrom(sock, &count, sizeof(int), 0, NULL, NULL); int n = ntohl(count); printf("has %d users logined server ", n); for (int i=0; i<n; i++) { USER_INFO user; recvfrom(sock, &user, sizeof(USER_INFO), 0, NULL, NULL); client_list.push_back(user); in_addr tmp; tmp.s_addr = user.ip; printf("%d %s <-> %s:%d ", i, user.username, inet_ntoa(tmp), ntohs(user.port)); } printf(" Commands are: "); printf("send username msg "); printf("list "); printf("exit "); printf(" "); fd_set rset; FD_ZERO(&rset); int nready; while (1) { FD_SET(STDIN_FILENO, &rset);//将标准输入加入到集合中 FD_SET(sock, &rset);//将sock套接字加入集合中 nready = select(sock+1, &rset, NULL, NULL, NULL); if (nready == -1) ERR_EXIT("select"); if (nready == 0) continue; if (FD_ISSET(sock, &rset)) { peerlen = sizeof(peeraddr); memset(&msg,0,sizeof(msg)); recvfrom(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&peeraddr, &peerlen); int cmd = ntohl(msg.cmd); //将服务端发过来的消息进行分发 switch (cmd) { case S2C_SOMEONE_LOGIN: do_someone_login(msg); break; case S2C_SOMEONE_LOGOUT: do_someone_logout(msg); break; case S2C_ONLINE_USER: do_getlist(sock); break; case C2C_CHAT: do_chat(msg); break; default: break; } } if (FD_ISSET(STDIN_FILENO, &rset)) {//标准输入产生了事件 char cmdline[100] = {0}; if (fgets(cmdline, sizeof(cmdline), stdin) == NULL) break; if (cmdline[0] == ' ') continue; cmdline[strlen(cmdline) - 1] = ' '; //对用户敲的命令进行解析处理 parse_cmd(cmdline, sock, &servaddr); } } }
下面来看一下parse_cmd函数的实现:
在看具体代码前,先看一下用户输入命令的几种情况:
下面具体来看一下该命令解析函数的实现:
首先从输入的字符中查找空格,并替换成' ',如下:
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr) { char cmd[10]={0}; char *p; p = strchr(cmdline, ' ');//检查空格 if (p != NULL) *p = '