
①、无连接
UDP协议它内部并没有维护端到端的一些连接状态,这跟TCP是不同的,TCP是基于连接的,而在连接的时候是需要进行三次握手,而UDP是不需要的。
②、基于消息的数据传输服务
对于TCP而言,它是基于流的数据传输服务,而在编程时,会遇到一个粘包问题,是需要我们进行处理的,而对于UDP来说不存在粘包问题,因为它是基于消息的数据传输服务,我们可以认为,这些数据包之间是有边界的,而TCP数据包之间是无边界的。
③、不可靠
这里面的不可靠主要表现在数据报可能会丢失,还可能会重复,还可能会乱序,以及缺乏流量控制,
④、一般情况下UDP更加高效。
这里提到了“一般情况”~

首先先看一下它的流程示意图:


下面就用编码的方式来认识一下UDP,在正式编码前,先看一下整个程序都需要用到哪些函数:

服务端echosrv.c:
首先第一个步骤是创建套接字:


第二个步骤:初使化地址,并绑定套接口:


相比TCP,UDP当绑定之后,并不需要监听,而可以直接接收客户端发来的消息了,所以接下来这一步是回射服务器:


接下来,来利用recvfrom、sendto两个函数来实现回射服务器的内容,首先来看一下recvfrom的函数原形:


如果成功接收消息后,接着得将消息用sendto发回给客户端,来看下它的函数原型:


接着编写客户端:echocli.c:

所以,接下来开始编写回射客户端的代码:


接下来接收从服务端回显过来的数据:

从以上代码的编写过程中,可以很直观的感受到UDP代码要比TCP代码简洁得多,下面编译运行一下:

可见一切运行正常,另外,请问下,客户端并没有与服务器端建立连接,也就是调用connect,那客户端是什么时候与服务端绑定的呢?
是在第一次sendto的时候就会绑定一个地址,也就是这句话:

对于sock而言,它有两个地址:
本地地址(也就是上图中说到的本地地址):可以通过getsockname来获取。
远程地址:可以通过getpeername来获取。
当第一次绑定成功之后, 之后就不会再次绑定了,关于UDP简单的实现就到这,贴上完整代码:
echosrv.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void echo_srv(int sock)
{
char recvbuf[1024] = {0};
struct sockaddr_in peeraddr;
socklen_t peerlen;
int n;
while (1)
{
peerlen = sizeof(peeraddr);
memset(recvbuf, 0, sizeof(recvbuf));
n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&peeraddr, &peerlen);
if (n == -1)
{
if (errno == EINTR)
continue;
ERR_EXIT("recvfrom");
}
else if (n > 0)
{
fputs(recvbuf, stdout);
sendto(sock, recvbuf, n, 0, (struct sockaddr*)&peeraddr, peerlen);
}
}
close(sock);
}
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
echo_srv(sock);//回射服务器
return 0;
}
echocli.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void echo_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");
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
if (ret == -1)
{
if (errno == EINTR)
continue;
ERR_EXIT("recvfrom");
}
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
}
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
echo_cli(sock);
return 0;
}

①、UDP报文可能会丢失、重复
针对数据可能会丢失,发送端就要启动一个定时器,在超时时间到的时候要重传,要有一个超时的处理机制,对于接收方也是一样的,如果对等方发送的数据丢失了,接收方也会一直阻塞,也应该要有这种超时的机制;针对发送的数据可能会重复,所以应用层应该要维护数据报之间的序号,也就是第二条提到的。
②、UDP报文可能会乱序
需要维护数据报之间的序号。
③、UDP缺乏流量控制
UDP有对应自己的一个缓冲区,当缓冲区满的时候,如果再往里面发送数据,并不是将数据给丢失掉,而是将数据覆盖掉原来的缓冲区,并没有像TCP那样的滑动窗口协议,达到流量控制的目的,其实可以模拟TCP滑动窗口协议来实现流量控制的目的。
④、UDP协议数据报文截断
如果接收到数据报大于我们接收的缓冲区,那么数据报文就会被截断,那些数据就已经被丢弃了,这边可以用一个例子来演示下,为了简单起见,客户端与服务端写在同一个文件中:


由于UDP是基于报式套接口,而不是基于流的,也就是说UDP只会接收对应大小的数据,其余的数据会从缓冲区中清除,由此可见,它不会产生粘包问题。
⑤、recvfrom返回0,不代表连接关闭,因为udp是无连接的。
当我们发送数据时,不发送任何一个字节的数据,返回值就是0。
⑥、ICMP异步错误
下面用一个实验场景来说明下,就是我们不启动服务端,而只是启动客户端,然后发送数据,这时会有什么反应呢?

从结果来看,客户端阻塞了,并没有捕捉到对等方没有启动的信息,那这现象跟“ICMP异步错误”有什么关系呢?
结合代码来分析:

所以这时就称之为异步的ICMP错误,按正常的情况下是需要在recvfrom才会被通知到,而在服务端没有开启时,不应该sendto成功,但是由于sendto只是完成了一个数据的拷贝,所以错误延迟到recvfrom的时候才能够被通知,而这时recvfrom其实也不能够被通知的,因为TCP规定这种ICMP错误是不能够返回给未连接的套接字的,所以说也得不到通知,recvfrom会一直阻塞,如果说能够收到错误通知,那肯定会退出了,因为代码已经做了错误判断,如下:

那如何解决此问题呢?采用下面这个方法:UDP connect。
⑦、UDP connect
其实UDP也是能调用connect的,在客户端加入如下代码,看是否解决了上面的问题,能够收到对等方未启动的错误呢?

下面来看下结果:

可见,在服务端没有开启的情况下,客户端这次收到了ICMP异步错误通知,通知是在recvfrom中返回的,因为此时的sock是已连接的套接字了,这就是UDP connect的一个作用,那UDP connect是否真的意味着建立了跟TCP一样的连接呢?肯定不是这样的,UDP在调connect的时候,并不会调TCP的三次握手操作,并没有跟对方传递数据,它仅仅只是维护了一个信息,这个sock跟对方维护了一种状态,通过这个套接字能够发送数据给对等方,而且只能够发送给对等方,实际上也就是该sock中的远程地址得到了绑定,那么这种sock就不能够发送给其它地址了,另外一点,一旦连接成功之后,客户端的sendto代码可以进行下面的改装:


可见效果一样,正常收发,另外,当sock是已连接套接口时,sendto也可以改用send函数来进行发送,改装如下:

这时就不演示了,效果是一样的,同样可以正常收发,可以UDP的connect的TCP的connect意义是不一样的。
⑧、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\n", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(cliaddr->sin_port));
client_list.push_back(user);
}
else /* 找到用户 */
{
printf("user %s has already logined\n", 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\n", 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\n", 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\n", username);
else if (cmd == S2C_LOGIN_OK)
{//证明用户已经成功登录了
printf("user %s has logined server\n", 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\n", 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\n", 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\n", 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\n", username);
else if (cmd == S2C_LOGIN_OK)
{//证明用户已经成功登录了
printf("user %s has logined server\n", username);
break;
}
}
int count;
recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);
int n = ntohl(count);
printf("has %d users logined server\n", 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\n", 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\n", 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\n", 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\n", 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\n", 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\n", 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\n", 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\n", username);
else if (cmd == S2C_LOGIN_OK)
{//证明用户已经成功登录了
printf("user %s has logined server\n", username);
break;
}
}
int count;
recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);
int n = ntohl(count);
printf("has %d users logined server\n", 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\n", i, user.username, inet_ntoa(tmp), ntohs(user.port));
}
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
}
接下来用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\n", username);
else if (cmd == S2C_LOGIN_OK)
{//证明用户已经成功登录了
printf("user %s has logined server\n", username);
break;
}
}
int count;
recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);
int n = ntohl(count);
printf("has %d users logined server\n", 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\n", i, user.username, inet_ntoa(tmp), ntohs(user.port));
}
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
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] == '\n')
continue;
cmdline[strlen(cmdline) - 1] = '\0';
//对用户敲的命令进行解析处理
parse_cmd(cmdline, sock, &servaddr);
}
}
}
下面来看一下parse_cmd函数的实现:
在看具体代码前,先看一下用户输入命令的几种情况:

下面具体来看一下该命令解析函数的实现:
首先从输入的字符中查找空格,并替换成'\0',如下:
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr)
{
char cmd[10]={0};
char *p;
p = strchr(cmdline, ' ');//检查空格
if (p != NULL)
*p = '\0';//将控格替换成\0
strcpy(cmd, cmdline);
}
然后下面对其输入的命令进行判断:
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr)
{
char cmd[10]={0};
char *p;
p = strchr(cmdline, ' ');
if (p != NULL)
*p = '\0';
strcpy(cmd, cmdline);
if (strcmp(cmd, "exit") == 0)
{//退出
}
else if (strcmp(cmd, "send") == 0)
{//向用户发送消息
}
else if (strcmp(cmd, "list") == 0)
{//列出在线用户列表
}
else
{//说明输入命令有误,给出正确命令提示
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
}
}
当用户敲入了"exit"命令时,会执行下面这段逻辑:
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr)
{
char cmd[10]={0};
char *p;
p = strchr(cmdline, ' ');
if (p != NULL)
*p = '\0';
strcpy(cmd, cmdline);
if (strcmp(cmd, "exit") == 0)
{//退出
MESSAGE msg;
memset(&msg,0,sizeof(msg));
msg.cmd = htonl(C2S_LOGOUT);//向服务器发送C2S_LOGOUT消息
strcpy(msg.body, username);
if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)servaddr, sizeof(struct sockaddr_in)) < 0)
ERR_EXIT("sendto");
printf("user %s has logout server\n", username);
exit(EXIT_SUCCESS);
}
else if (strcmp(cmd, "send") == 0)
{//向用户发送消息
}
else if (strcmp(cmd, "list") == 0)
{//列出在线用户列表
}
else
{//说明输入命令有误,给出正确命令提示
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
}
}
当用户向其它用户发送聊天信息的话:
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr)
{
char cmd[10]={0};
char *p;
p = strchr(cmdline, ' ');
if (p != NULL)
*p = '\0';
strcpy(cmd, cmdline);
if (strcmp(cmd, "exit") == 0)
{//退出
MESSAGE msg;
memset(&msg,0,sizeof(msg));
msg.cmd = htonl(C2S_LOGOUT);
strcpy(msg.body, username);
if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)servaddr, sizeof(struct sockaddr_in)) < 0)
ERR_EXIT("sendto");
printf("user %s has logout server\n", username);
exit(EXIT_SUCCESS);
}
else if (strcmp(cmd, "send") == 0)
{//向用户发送消息
char peername[16]={0};//要发送的用户名
char msg[MSG_LEN]={0};//要发送的消息
//下面则开始解析命令
/* send user msg */
/* p p2 */
while (*p++ == ' ') ;
char *p2;
p2 = strchr(p, ' ');
if (p2 == NULL)
{
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
return;
}
*p2 = '\0';
strcpy(peername, p);
while (*p2++ == ' ') ;
strcpy(msg, p2);
//然后将消息发送给对方,这里封装了一个方法
sendmsgto(sock, peername, msg);
}
else if (strcmp(cmd, "list") == 0)
{//列出在线用户列表
}
else
{//说明输入命令有误,给出正确命令提示
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
}
}
下面来看一下sendmsgto方法的具体实现:
bool sendmsgto(int sock, char* name, char* msg)
{
if (strcmp(name, username) == 0)
{//如果向当前用户发送消息,给出错误提示
printf("can't send message to self\n");
return false;
}
return true;
}
bool sendmsgto(int sock, char* name, char* msg)
{
if (strcmp(name, username) == 0)
{//如果向当前用户发送消息,给出错误提示
printf("can't send message to self\n");
return false;
}
//下面开始遍历要发送的用户是否已经登录
USER_LIST::iterator it;
for (it=client_list.begin(); it != client_list.end(); ++it)
{
if (strcmp(it->username,name) == 0)
break;
}
if (it == client_list.end())
{//说明要发送的用户还没有登录,给出错误提示
printf("user %s has not logined server\n", name);
return false;
}
return true;
}
bool sendmsgto(int sock, char* name, char* msg)
{
if (strcmp(name, username) == 0)
{//如果向当前用户发送消息,给出错误提示
printf("can't send message to self\n");
return false;
}
USER_LIST::iterator it;
for (it=client_list.begin(); it != client_list.end(); ++it)
{
if (strcmp(it->username,name) == 0)
break;
}
if (it == client_list.end())
{
printf("user %s has not logined server\n", name);
return false;
}
//流程走到这,证明要发送的用户是已经成功登录过的,所以接下来是组拼消息
MESSAGE m;
memset(&m,0,sizeof(m));
m.cmd = htonl(C2C_CHAT);//向服务器发送C2C_CHAT命令
CHAT_MSG cm;
strcpy(cm.username, username);
strcpy(cm.msg, msg);
memcpy(m.body, &cm, sizeof(cm));
//strcpy(m.body,msg);
struct sockaddr_in peeraddr;
memset(&peeraddr,0,sizeof(peeraddr));
peeraddr.sin_family = AF_INET;
peeraddr.sin_addr.s_addr = it->ip;
peeraddr.sin_port = it->port;
in_addr tmp;
tmp.s_addr = it->ip;
printf("sending message [%s] to user [%s] <-> %s:%d\n", msg, name, inet_ntoa(tmp), ntohs(it->port));
//发送消息
sendto(sock, (const char*)&m, sizeof(m), 0, (struct sockaddr *)&peeraddr, sizeof(peeraddr));
return true;
}
如果是输入的在线用户列表的命令,则会走如下逻辑:
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr)
{
char cmd[10]={0};
char *p;
p = strchr(cmdline, ' ');
if (p != NULL)
*p = '\0';
strcpy(cmd, cmdline);
if (strcmp(cmd, "exit") == 0)
{//退出
MESSAGE msg;
memset(&msg,0,sizeof(msg));
msg.cmd = htonl(C2S_LOGOUT);
strcpy(msg.body, username);
if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)servaddr, sizeof(struct sockaddr_in)) < 0)
ERR_EXIT("sendto");
printf("user %s has logout server\n", username);
exit(EXIT_SUCCESS);
}
else if (strcmp(cmd, "send") == 0)
{//向用户发送消息
char peername[16]={0};
char msg[MSG_LEN]={0};
/* send user msg */
/* p p2 */
while (*p++ == ' ') ;
char *p2;
p2 = strchr(p, ' ');
if (p2 == NULL)
{
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
return;
}
*p2 = '\0';
strcpy(peername, p);
while (*p2++ == ' ') ;
strcpy(msg, p2);
sendmsgto(sock, peername, msg);
}
else if (strcmp(cmd, "list") == 0)
{//列出在线用户列表
MESSAGE msg;
memset(&msg, 0, sizeof(msg));
msg.cmd = htonl(C2S_ONLINE_USER);
if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)servaddr, sizeof(struct sockaddr_in)) < 0)
ERR_EXIT("sendto");
}
else
{//说明输入命令有误,给出正确命令提示
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
}
}
至此parse_cmd函数的实现就分析到这,客户端还有可能会收到服务端发来的网络的消息,所以下面来看一下这些消息的分发:

当有用户登录了,do_someone_login函数实现如下:

当有用户登出了,do_someone_logout函数实现如下:

当用户要获取当前在线用户列表时,do_getlist函数实现如下:
void do_getlist(int sock)
{
int count;
recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);//首先得到用户列表的总个数
printf("has %d users logined server\n", ntohl(count));
client_list.clear();//将当前的在线列表清空
int n = ntohl(count);
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("%s <-> %s:%d\n", user.username, inet_ntoa(tmp), ntohs(user.port));
}
}
当要发送消息时,do_chat函数消息实现如下:
void do_chat(const MESSAGE& msg)
{
CHAT_MSG *cm = (CHAT_MSG*)msg.body;
printf("recv a msg [%s] from [%s]\n", cm->msg, cm->username);
//recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);
}
chatsrv.cpp:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include "pub.h"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
// 聊天室成员列表
USER_LIST client_list;
void do_login(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr);
void do_logout(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr);
void do_sendlist(int sock, struct sockaddr_in *cliaddr);
void chat_srv(int sock)
{
struct sockaddr_in cliaddr;
socklen_t clilen;
int n;
MESSAGE msg;
while (1)
{
memset(&msg, 0, sizeof(msg));
clilen = sizeof(cliaddr);
n = recvfrom(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&cliaddr, &clilen);
if (n < 0)
{
if (errno == EINTR)
continue;
ERR_EXIT("recvfrom");
}
int cmd = ntohl(msg.cmd);
switch (cmd)
{
case C2S_LOGIN:
do_login(msg, sock, &cliaddr);
break;
case C2S_LOGOUT:
do_logout(msg, sock, &cliaddr);
break;
case C2S_ONLINE_USER:
do_sendlist(sock, &cliaddr);
break;
default:
break;
}
}
}
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\n", 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\n", 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\n", 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));
}
}
void do_logout(MESSAGE& msg, int sock, struct sockaddr_in *cliaddr)
{
printf("has a user logout : %s <-> %s:%d\n", msg.body, inet_ntoa(cliaddr->sin_addr), ntohs(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())
client_list.erase(it);
// 向其他用户通知有用户登出
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_LOGOUT);
if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)&peeraddr, sizeof(peeraddr)) < 0)
ERR_EXIT("sendto");
}
}
void do_sendlist(int sock, struct sockaddr_in *cliaddr)
{
MESSAGE msg;
msg.cmd = htonl(S2C_ONLINE_USER);
sendto(sock, (const char*)&msg, sizeof(msg), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in));
int count = htonl((int)client_list.size());
/* 发送在线用户数 */
sendto(sock, (const char*)&count, sizeof(int), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in));
/* 发送在线用户列表 */
for (USER_LIST::iterator it=client_list.begin(); it != client_list.end(); ++it)
{
sendto(sock, &*it, sizeof(USER_INFO), 0, (struct sockaddr *)cliaddr, sizeof(struct sockaddr_in));
}
}
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
chat_srv(sock);
return 0;
}
chatcli.cpp:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include "pub.h"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
// 当前用户名
char username[16];
// 聊天室成员列表
USER_LIST client_list;
void do_someone_login(MESSAGE& msg);
void do_someone_logout(MESSAGE& msg);
void do_getlist();
void do_chat();
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr);
bool sendmsgto(int sock, char* username, char* msg);
void parse_cmd(char* cmdline, int sock, struct sockaddr_in *servaddr)
{
char cmd[10]={0};
char *p;
p = strchr(cmdline, ' ');
if (p != NULL)
*p = '\0';
strcpy(cmd, cmdline);
if (strcmp(cmd, "exit") == 0)
{//退出
MESSAGE msg;
memset(&msg,0,sizeof(msg));
msg.cmd = htonl(C2S_LOGOUT);
strcpy(msg.body, username);
if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)servaddr, sizeof(struct sockaddr_in)) < 0)
ERR_EXIT("sendto");
printf("user %s has logout server\n", username);
exit(EXIT_SUCCESS);
}
else if (strcmp(cmd, "send") == 0)
{//向用户发送消息
char peername[16]={0};
char msg[MSG_LEN]={0};
/* send user msg */
/* p p2 */
while (*p++ == ' ') ;
char *p2;
p2 = strchr(p, ' ');
if (p2 == NULL)
{
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
return;
}
*p2 = '\0';
strcpy(peername, p);
while (*p2++ == ' ') ;
strcpy(msg, p2);
sendmsgto(sock, peername, msg);
}
else if (strcmp(cmd, "list") == 0)
{//列出在线用户列表
MESSAGE msg;
memset(&msg, 0, sizeof(msg));
msg.cmd = htonl(C2S_ONLINE_USER);
if (sendto(sock, &msg, sizeof(msg), 0, (struct sockaddr *)servaddr, sizeof(struct sockaddr_in)) < 0)
ERR_EXIT("sendto");
}
else
{//说明输入命令有误,给出正确命令提示
printf("bad command\n");
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
}
}
bool sendmsgto(int sock, char* name, char* msg)
{
if (strcmp(name, username) == 0)
{
printf("can't send message to self\n");
return false;
}
USER_LIST::iterator it;
for (it=client_list.begin(); it != client_list.end(); ++it)
{
if (strcmp(it->username,name) == 0)
break;
}
if (it == client_list.end())
{
printf("user %s has not logined server\n", name);
return false;
}
MESSAGE m;
memset(&m,0,sizeof(m));
m.cmd = htonl(C2C_CHAT);
CHAT_MSG cm;
strcpy(cm.username, username);
strcpy(cm.msg, msg);
memcpy(m.body, &cm, sizeof(cm));
//strcpy(m.body,msg);
struct sockaddr_in peeraddr;
memset(&peeraddr,0,sizeof(peeraddr));
peeraddr.sin_family = AF_INET;
peeraddr.sin_addr.s_addr = it->ip;
peeraddr.sin_port = it->port;
in_addr tmp;
tmp.s_addr = it->ip;
printf("sending message [%s] to user [%s] <-> %s:%d\n", msg, name, inet_ntoa(tmp), ntohs(it->port));
sendto(sock, (const char*)&m, sizeof(m), 0, (struct sockaddr *)&peeraddr, sizeof(peeraddr));
return true;
}
void do_getlist(int sock)
{
int count;
recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);//首先得到用户列表的总个数
printf("has %d users logined server\n", ntohl(count));
client_list.clear();//将当前的在线列表清空
int n = ntohl(count);
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("%s <-> %s:%d\n", user.username, inet_ntoa(tmp), ntohs(user.port));
}
}
void do_someone_login(MESSAGE& msg)
{
USER_INFO *user = (USER_INFO*)msg.body;
in_addr tmp;
tmp.s_addr = user->ip;
printf("%s <-> %s:%d has logined server\n", user->username, inet_ntoa(tmp), ntohs(user->port));
client_list.push_back(*user);
}
void do_someone_logout(MESSAGE& msg)
{
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())
client_list.erase(it);
printf("user %s has logout server\n", msg.body);
}
void do_chat(const MESSAGE& msg)
{
CHAT_MSG *cm = (CHAT_MSG*)msg.body;
printf("recv a msg [%s] from [%s]\n", cm->msg, cm->username);
//recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);
}
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\n", username);
else if (cmd == S2C_LOGIN_OK)
{//证明用户已经成功登录了
printf("user %s has logined server\n", username);
break;
}
}
int count;
recvfrom(sock, &count, sizeof(int), 0, NULL, NULL);
int n = ntohl(count);
printf("has %d users logined server\n", 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\n", i, user.username, inet_ntoa(tmp), ntohs(user.port));
}
printf("\nCommands are:\n");
printf("send username msg\n");
printf("list\n");
printf("exit\n");
printf("\n");
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] == '\n')
continue;
cmdline[strlen(cmdline) - 1] = '\0';
//对用户敲的命令进行解析处理
parse_cmd(cmdline, sock, &servaddr);
}
}
}
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
chat_cli(sock);
return 0;
}

①、UNIX域套接字与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍。
UNIX域协议主要是用于本地的进程间进行通讯,而TCP的套接字不仅可以用于本地的进程间进行通讯,还可用于两台不同主机上面进程间进行通讯,如果都是用于本地的进程间通讯的话,UNIX域协议比TCP协议效率来得高。
②、UNIX域套接字可以在同一台主机上各进程之间传递描述符。
也就是可以传递一个文件,关于这个知识下一次再学习,稍复杂一些~
③、UNIX域套接字与传统套接字的区别是用路径名来表示协议族的描述。


而对于之前我们用的网际的IPV4的地址结构是sockaddr_in,如下:

其实结构都差不多,下面,用代码来用UNIX域协议来实现回射客户/服务程序。

服务端echosrv.c:
首先创建一个监听套接口:

在TCP编程中,在正式绑定监听套接字之前是需要设备地址重复利用的,如下:

而对于UNIX域套接字而言,这一步就不用了,这是与TCP协议不同的,下面则开始绑定:

接下来则进行监听:

其中SOMAXCONN是最大连接,可以从listen的man帮助中找到:

下面则处理客户端发过来的请求,这里简单起见,就用fork进程的方式来处理多个客户端,而不用select方式处理并发了:

下面来处理客户端的连接:

另外注意:这里需要引入一个新的头文件:

下面来编写客户端echocli.c:
首先也是创业套接口:

接着连接服务器:

当连接成功之后,就执行回射客户端的函数:

具体实现基本跟TCP的类似,也比较容易理解:

下面开始编译运行:

这也就说明了这句代码的意义,是在bind的时候产生该文件的:

靠这个文件实现两者的互通,来观察一下它的类型:

其中可以通过命令来查看linux下的文件类型,其中就有一个套接字文件:


但是有一个问题,如果我再重新运行服务端:

如何解决这个问题呢,对于TCP来说可以设置地址重复利用既可,但是对于UNIX域协议来说,可以在重新启动服务端时,将这个路径文件删除既可:


①、bind成功将会创建一个文件,权限为0777 & ~umask
下面来看一下产生的套接字的文件的权限:

而当前的umask为:

755=0777 & (~0022)
②、sun_path最好用一个绝对路径
如果用相对路径会出现什么样的问题呢?下面来做个实验,就是将客户端与服务器程序放到不同的目录里面:


所以,为了避免当客户端与服务端程序在不同目录上的问题,可以将文件路径改为绝对的,这里将此文件放到tmp目录中,如下:


下面编译之后,再将客户端程序拷贝到上级目录中,让它与服务端不在同一个目录,如下:

③、UNIX域协议支持流式套接口与报式套接口
基于流式的套接口是需要处理粘包问题,实际上上面写的程序是没有处理粘包问题的,实现思路跟TCP的一样,这里就不演示了;如果是报式套接口就不存在粘包问题。
④、UNIX域流式套接字connect发现监听队列满时,会立刻返回一个ECONNREFUSED,这和TCP不同,如果监听队列满,会忽略到来的SYN,这导致对方重传SYN


实际上sockpair有点像之前linux系统编程中学习的pipe匿名管道,匿名管道它是半双工的,只能用于亲缘关系的进程间进行通信,也就是说父子进程或兄弟进程间进行通讯,因为它是没有名称的,父子进程可以通过共享描述符的方式来进行通信,子进程继承了父进程的文件描述符,从而达到了通信的目的。而今天学习的sockpair是一个全双工的流管道,其它也一样,也只能用于父子进程或亲缘关系之间进行通讯,所以其中sv套接字对就很容易理解了,但是跟pipe有些区别,先来回顾下pipe:

其中的fd也是套接字对,一端表示读,一端表示写,而sockpair中的sv[0]、sv[1]两端分别既可以表示读端,也可以表示写端。
认识了sockpair函数原形,下面用程序来说明它的用法:
首先创业一个套接字对:

由于它也是只能用于父子进程或亲缘关系之间进行通讯,所以需要fork进程出来:

下面就来实现父子进程进行通讯:

而对于子进程而言,代码基本类似:

编译运行看结果:

从结果运行来看,通过sockpair就完成了全双工的通讯。

学习这两个函数的目的,是为了通过UNIX域协议如何传递文件描述字,关于这个函数的使用会比较复杂,需慢慢理解。
首先来查看一下man帮助:

其中第二个参数是msghdr结构体,所以有必要来研究一下这个结构体:

哇,这个结构体貌似挺复杂的,下面一一来熟悉其字段含义:


这时,需要来看另外一个函数了,该结构体在其中有介绍到:

那怎么理解该参数呢?这个需要从send函数来分析:

所以iovec结构体的字段就可以从send的这两个参数来理解:

并且,可以发现:

下面来看一个示意图:


从上面示意图中可以发现,如果用sendmsg函数,就可以发送多个缓冲区的数据了,而如果用send只能发送一个缓冲区,所以从这也可以看出sendmsg的强大。
如果说要传递文件描述字,还需要发送一些辅助的数据,这些辅助数据是一些控制信息,也就是下面这些参数:


而其中msg_control是指向一个结构体,那它长啥样呢?需要从另外一个函数的帮助文档中得知:

那具体属性的含议是啥呢?

实际上,在填充这些数据的时候,并没有这么简单,它还会按照一定的方式来进行对齐,接下来再来看另外一个示意图---辅助数据的示意图:



其中可以看到定义了一些宏,这是由于:

所以,下面来认识一下这些宏定义:

其中"size_t CMSG_SPACE(size_t length)",结合图来说明就是:

大致了解了以上这些数据结构,下面则可以开始编写代码来传递描述字了,但是代码会比较复杂,可以一步步来理解,下面开始。

实际上,就是能过以下两个函数来封装发送和接收文件描述字的功能,如下:

首先封装发送文件描述字的方法:

下面一步步来实现该函数,首先准备第二个参数:

所以,先声明一个该结构体:

接下来填充里面的各个字段,还是看图说话:


接下来指定缓冲区:


最后则要准备辅助数据了,因为我们是发送文件描述字,这也是最关键的:

所以msg_control需要指向一个辅助数据的缓冲区,其大小根据发送的文件描述符来获得,如下:

接下来,则需要准备缓冲区中cmsghdr中的数据,也就是发送文件描述字主要是靠它:


另外关于数据的填充我们不需要关心,因为都是用系统提供的宏来操作数据的,当所有的数据都准备好之后,下面则可以开始发送了:

接下来,则需要封装一个接收文件描述字的函数了,由于怎么发送文件描述字已经很明白了,所以接收也就很简单了,基本类似,这里面就不一一进行说明了:

以上发送和接收文件描述字的函数都已经封装好了,接下来利用这两个函数来实现文件描述字的真正传递实验,实验的思路是这样:如果父进程打开了一个文件描述字,再fork()时,子进程是能共享父进程的文件描述字的,也就是只要在fork()之前,打开文件描述字,子进程就能共享它;但是当fork()进程之后,如果一个子进程打开一个文件描述字,父进程是无法共享获取的,所以,这里就可以利用这个原理,来将文件描述字从子进程传递给父进程,还是用sockpair函数,具体如下:


另外,文件描述字的传递,只能通过UNIX域协议的套接字,当前是利用了sockpair函数来实现了父子进程文件描述字的传递,而如果要实现不相关的两个进程之间传递,就不能用socketpair了,就得用上一节中介绍的UNIX域套接字来进行传递,而普通的TCP套接字是不能传递文件描述字的,这个是需要明白了。
最后贴出代码:
send_fd.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void send_fd(int sock_fd, int send_fd)
{
struct msghdr msg;
struct iovec vec;
struct cmsghdr *p_cmsg;
char sendchar = 0;
vec.iov_base = &sendchar;
vec.iov_len = sizeof(sendchar);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &vec;
msg.msg_iovlen = 1;
char cmsgbuf[CMSG_SPACE(sizeof(send_fd))];
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
p_cmsg = CMSG_FIRSTHDR(&msg);
p_cmsg->cmsg_level = SOL_SOCKET;
p_cmsg->cmsg_type = SCM_RIGHTS;
p_cmsg->cmsg_len = CMSG_LEN(sizeof(send_fd));
int *p_fds;
p_fds = (int*)CMSG_DATA(p_cmsg);
*p_fds = send_fd;
int ret;
ret = sendmsg(sock_fd, &msg, 0);
if (ret != 1)
ERR_EXIT("sendmsg");
}
int recv_fd(const int sock_fd)
{
int ret;
struct msghdr msg;
char recvchar;
struct iovec vec;
int recv_fd;
char cmsgbuf[CMSG_SPACE(sizeof(recv_fd))];
struct cmsghdr *p_cmsg;
int *p_fd;
vec.iov_base = &recvchar;
vec.iov_len = sizeof(recvchar);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &vec;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
msg.msg_flags = 0;
p_fd = (int*)CMSG_DATA(CMSG_FIRSTHDR(&msg));
*p_fd = -1;
ret = recvmsg(sock_fd, &msg, 0);
if (ret != 1)
ERR_EXIT("recvmsg");
p_cmsg = CMSG_FIRSTHDR(&msg);
if (p_cmsg == NULL)
ERR_EXIT("no passed fd");
p_fd = (int*)CMSG_DATA(p_cmsg);
recv_fd = *p_fd;
if (recv_fd == -1)
ERR_EXIT("no passed fd");
return recv_fd;
}
int main(void)
{
int sockfds[2];
if (socketpair(PF_UNIX, SOCK_STREAM, 0, sockfds) < 0)
ERR_EXIT("socketpair");
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork");
if (pid > 0)
{
close(sockfds[1]);
int fd = recv_fd(sockfds[0]);
char buf[1024] = {0};
read(fd, buf, sizeof(buf));
printf("buf=%s\n", buf);
}
else if (pid == 0)
{
close(sockfds[0]);
int fd;
fd = open("test.txt", O_RDONLY);
if (fd == -1);
send_fd(sockfds[1], fd);
}
return 0;
}