①、無鏈接java
UDP協議它內部並無維護端到端的一些鏈接狀態,這跟TCP是不一樣的,TCP是基於鏈接的,而在鏈接的時候是須要進行三次握手,而UDP是不須要的。linux
②、基於消息的數據傳輸服務c++
對於TCP而言,它是基於流的數據傳輸服務,而在編程時,會遇到一個粘包問題,是須要咱們進行處理的,而對於UDP來講不存在粘包問題,由於它是基於消息的數據傳輸服務,咱們能夠認爲,這些數據包之間是有邊界的,而TCP數據包之間是無邊界的。編程
③、不可靠bash
這裏面的不可靠主要表如今數據報可能會丟失,還可能會重複,還可能會亂序,以及缺少流量控制,服務器
④、通常狀況下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;
}
複製代碼