引言
本文是圍繞Linux udp api 構建一個簡易的多人聊天室.重點看思路,幫助我們加深
對udp開發中一些api了解.相對而言udp socket開發相比tcp socket開發注意的細節要少很多.
但是水也很深. 本文就當是一個demo整合幫助開發者回顧和繼續了解 linux udp開發的基本流程.
首先我們來看看 linux udp 和 tcp的異同.
/*
這裡簡單比較一下TCP和UDP在編程實現上的一些區別:
TCP流程
建立一個TCP連接需要三次握手,而斷開一個TCP則需要四個分節。當某個應用進程調用close(主動端)後(可以是服務器端,也可以是客戶
端),這一端的TCP發送一個FIN,表示數據發送完畢;另一端(被動端)發送一個確認,當被動端待處理的應用進程都處理完畢後,發送一個FIN到主動
端,並關閉套接口,主動端接收到這個FIN後再發送一個確認,到此為止這個TCP連接被斷開。
UDP套接口
UDP套接口是無連接的、不可靠的數據報協議;既然他不可靠為什麼還要用呢?
其一:當應用程序使用廣播或多播是只能使用UDP協議;
其二:由於它是無連接的,所以速度快。因為UDP套接口是無連接的,如果一方的數據報丟失,那另一方將無限等待,解決辦法是設置一個超時。
在編寫UDP套接口程序時,有幾點要注意:建立套接口時socket函數的第二個參數應該是SOCK_DGRAM,說明是建立一個UDP套接口;
由於UDP是無連接的,所以服務器端並不需要listen或accept函數;
當UDP套接口調用connect函數時,內核只記錄連接放的IP地址 和端口,並立即返回給調用進程.
*/
參照
linux udp api簡介 http://blog.csdn.net/wocjj/article/details/8315559
tcp 和udp區別 http://www.cnblogs.com/Jessy/p/3536163.html
這裡簡單引述一下 udp相比tcp 用到的兩個api . recvfrom()/sendto() 具體細節如下
#include <sys/types.h> #include <sys/socket.h> /* * 這兩個函數基本等同於 一個 send 和 recv . 詳細參數解釋如下 * s : 文件描述符,等同於 socket返回的值 * buf : 數據其實地址 * len : 發送數據長度或接受數據緩沖區最大長度 * flags : 發送標識,默認就用O.帶外數據使用 MSG_OOB, 偷窺用MSG_PEEK ..... * addr : 發送的網絡地址或接收的網絡地址 * alen : sento標識地址長度做輸入參數, recvfrom表示輸入和輸出參數.可以為NULL此時addr也要為NULL * : 返回0表示執行成功,否則返回<0 . 更多細節查詢man手冊 */ extern int sendto (int s, const void *buf, int len, unsigned int flags, const struct sockaddr *addr, int alen); extern int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *addr, int *alen);
上面就是兩個函數的大致用法. 具體可以查看linux api幫助手冊. 最好就用 man sendto / man recvfrom 把那一系列函數都看看.
現在很多文章都是轉載,但是找不見轉載的地址, 下面會舉一個簡易的UDP回顯服務器的demo加深理解.
前言
首先看設計圖

有點low. 簡單看看吧. 那我們先看 客戶端代碼 udpclt.c 代碼
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
// 測試端口和網絡地址
#define _INT_PORT (8088)
#define _INT_BUF 1024
// udp 服務器主函數
int main(int argc, char* argv[]) {
int sd, len;
struct sockaddr_in addr = { AF_INET };
socklen_t alen = sizeof addr;
char msg[_INT_BUF];
//創建服務器socket 地址,客戶端給它發送信息
if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
perror("main socket ");
exit(sd);
}
// 這裡簡單輸出連接信息
printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT);
//拼接對方地址
addr.sin_port = htons(_INT_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){
perror("main bind ");
exit(-1);
}
// 循環處理消息讀取發送到客戶端
while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){
msg[len] = '\0';
printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), msg);
//這裡發送信息過去, 也可以事先connect這裡就不綁定了
sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen);
}
close(sd);
puts("udp server end ------------------------------<");
return 0;
}
編譯是
gcc -g -Wall -o udpclt.out udpclt.c
udp 服務器 udpsrv.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
// 測試端口和網絡地址
#define _INT_PORT (8088)
#define _INT_BUF 1024
// udp 服務器主函數
int main(int argc, char* argv[]) {
int sd, len;
struct sockaddr_in addr = { AF_INET };
socklen_t alen = sizeof addr;
char msg[_INT_BUF];
//創建服務器socket 地址,客戶端給它發送信息
if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
perror("main socket ");
exit(sd);
}
// 這裡簡單輸出連接信息
printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT);
//拼接對方地址
addr.sin_port = htons(_INT_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){
perror("main bind ");
exit(-1);
}
// 循環處理消息讀取發送到客戶端
while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){
msg[len] = '\0';
printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), msg);
//這裡發送信息過去, 也可以事先connect這裡就不綁定了
sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen);
}
close(sd);
puts("udp server end ------------------------------<");
return 0;
}
編譯是
gcc -g -Wall -o udpsrv.out udpsrv.c
後面運行結果如下 udp服務器如下 (Ctrl + C 退出)

udp 客戶端如下 (Ctrl + D 結束輸入)

到這裡將上面代碼 敲一遍基本上udp 一套api就會使用了. 後面進入正題設計聊天室代碼.
正文
首先看客戶端設計代碼. 主要思路是子進程處理數據的輸出, 父進程處理服務器數據的接收. 具體設計如下(畫的圖有點low就不畫了.../(ㄒoㄒ)/~~)
udpmulclt.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
// 名字長度包含'\0'
#define _INT_NAME (64)
// 報文最大長度,包含'\0'
#define _INT_TEXT (512)
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
* 簡單的Linux上API錯誤判斷檢測宏, 好用值得使用
*/
#define IF_CHECK(code) \
if((code) < 0) \
CERR_EXIT(#code)
// 發送和接收的信息體
struct umsg{
char type; //協議 '1' => 向服務器發送名字, '2' => 向服務器發送信息, '3' => 向服務器發送退出信息
char name[_INT_NAME]; //保存用戶名字
char text[_INT_TEXT]; //得到文本信息,空間換時間
};
/*
* udp聊天室的客戶端, 子進程發送信息,父進程接受信息
*/
int main(int argc, char* argv[]) {
int sd, rt;
struct sockaddr_in addr = { AF_INET };
socklen_t alen = sizeof addr;
pid_t pid;
struct umsg msg = { '1' };
// 這裡簡單檢測
if(argc != 4) {
fprintf(stderr, "uage : %s [ip] [port] [name]\n", argv[0]);
exit(-1);
}
// 下面對接數據
if((rt = atoi(argv[2]))<1024 || rt > 65535)
CERR("atoi port = %s is error!", argv[2]);
// 接著判斷ip數據
IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
addr.sin_port = htons(rt);
// 這裡拼接用戶名字
strncpy(msg.name, argv[3], _INT_NAME - 1);
//創建socket 連接
IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
// 這裡就是發送登錄信息給udp聊天服務器了
IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
//開啟一個進程, 子進程處理發送信息, 父進程接收信息
IF_CHECK(pid = fork());
if(pid == 0) { //子進程,先忽略退出處理防止成為僵屍進程
signal(SIGCHLD, SIG_IGN);
while(fgets(msg.text, _INT_TEXT, stdin)){
if(strcasecmp(msg.text, "quit\n") == 0){ //表示退出
msg.type = '3';
// 發送數據並檢測
IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
break;
}
// 洗唛按發送普通信息
msg.type = '2';
IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
}
// 處理結算操作,並殺死父進程
close(sd);
kill(getppid(), SIGKILL);
exit(0);
}
// 這裡是父進程處理數據的讀取
for(;;){
bzero(&msg, sizeof msg);
IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '\0';
switch(msg.type){
case '1':printf("%s 登錄了聊天室!\n", msg.name);break;
case '2':printf("%s 說了: %s\n", msg.name, msg.text);break;
case '3':printf("%s 退出了聊天室!\n", msg.name);break;
default://未識別的異常報文,程序直接退出
fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port), msg.type, msg.name, msg.text);
goto __exit;
}
}
__exit:
// 殺死並等待子進程退出
close(sd);
kill(pid, SIGKILL);
waitpid(pid, NULL, -1);
return 0;
}
這裡主要需要注意的是
// 發送和接收的信息體
struct umsg{
char type; //協議 '1' => 向服務器發送名字, '2' => 向服務器發送信息, '3' => 向服務器發送退出信息
char name[_INT_NAME]; //保存用戶名字
char text[_INT_TEXT]; //得到文本信息,空間換時間
};
傳輸和接收的數據格式, type表示協議或行為. 我這裡細心了處理 name, text最後一個字符必須是 '\0'. 其它都是業務代碼.再扯一點
struct sockaddr_in addr = { AF_INET };
等價於
struct sockaddr_in addr; memset(&addr, 0, sizeof addr); addr.sin_family = AF_INET;
也是一個C開發中技巧吧. 再扯一點linux上提供 bzero函數, 但是window上沒有. 寫了個通用的如下
//7.0 置空操作
#ifndef BZERO
//v必須是個變量
#define BZERO(v) \
memset(&v,0,sizeof(v))
#endif/* !BZERO */
可以試試吧畢竟跨平台....
好了那我們說 udp 聊天室的服務器設計思路. 就是服務器會維護一個客戶端鏈表. 有信息來就廣播. 好簡單吧.就是這樣.正常的事都簡單.
簡單的是美的. 好了看代碼總設計和實現. udpmulsrv.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
// 名字長度包含'\0'
#define _INT_NAME (64)
// 報文最大長度,包含'\0'
#define _INT_TEXT (512)
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
* 簡單的Linux上API錯誤判斷檢測宏, 好用值得使用
*/
#define IF_CHECK(code) \
if((code) < 0) \
CERR_EXIT(#code)
// 發送和接收的信息體
struct umsg{
char type; //協議 '1' => 向服務器發送名字, '2' => 向服務器發送信息, '3' => 向服務器發送退出信息
char name[_INT_NAME]; //保存用戶名字
char text[_INT_TEXT]; //得到文本信息,空間換時間
};
// 維護一個客戶端鏈表信息,記錄登錄信息
typedef struct ucnode {
struct sockaddr_in addr;
struct ucnode* next;
} *ucnode_t ;
// 新建一個結點對象
static inline ucnode_t _new_ucnode(struct sockaddr_in* pa){
ucnode_t node = calloc(sizeof(struct ucnode), 1);
if(NULL == node)
CERR_EXIT("calloc sizeof struct ucnode is error. ");
node->addr = *pa;
return node;
}
// 插入數據,這裡head默認頭結點是當前服務器結點
static inline void _insert_ucnode(ucnode_t head, struct sockaddr_in* pa) {
ucnode_t node = _new_ucnode(pa);
node->next = head->next;
head->next = node;
}
// 這裡是有用戶登錄處理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
_insert_ucnode(head, pa);
head = head->next;
// 從此之後才為以前的鏈表
while(head->next){
head = head->next;
IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
}
}
// 信息廣播
static void _broadcast_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
int flag = 0; //1表示已經找到了
while(head->next) {
head = head->next;
if((flag) || !(flag=memcmp(pa, &head->addr, sizeof(struct sockaddr_in))==0)){
IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
}
}
}
// 有人退出群聊
static void _quit_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
int flag = 0;//1表示已經找到
while(head->next) {
if((flag) || !(flag = memcmp(pa, &head->next->addr, sizeof(struct sockaddr_in))==0)){
IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->next->addr, sizeof(struct sockaddr_in)));
head = head->next;
}
else { //刪除這個退出的用戶
ucnode_t tmp = head->next;
head->next = tmp->next;
free(tmp);
}
}
}
// 銷毀維護的對象池,沒有往復雜的考慮了簡單處理退出了
static void _destroy_ucnode(ucnode_t* phead) {
ucnode_t head;
if((!phead) || !(head=*phead)) return;
while(head){
ucnode_t tmp = head->next;
free(head);
head = tmp;
}
*phead = NULL;
}
/*
* udp聊天室的服務器, 子進程廣播信息,父進程接受信息
*/
int main(int argc, char* argv[]) {
int sd, rt;
struct sockaddr_in addr = { AF_INET };
socklen_t alen = sizeof addr;
struct umsg msg;
ucnode_t head;
// 這裡簡單檢測
if(argc != 3) {
fprintf(stderr, "uage : %s [ip] [port]\n", argv[0]);
exit(-1);
}
// 下面對接數據
if((rt = atoi(argv[2]))<1024 || rt > 65535)
CERR("atoi port = %s is error!", argv[2]);
// 接著判斷ip數據
IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
addr.sin_port = htons(rt); //端口要采用網絡字節序
// 創建socket
IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
// 這裡bind綁定設置的地址
IF_CHECK(bind(sd, (struct sockaddr*)&addr, alen));
//開始監聽了
head = _new_ucnode(&addr);
for(;;){
bzero(&msg, sizeof msg);
IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '\0';
fprintf(stdout, "msg is [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port), msg.type, msg.name, msg.text);
// 開始判斷處理
switch(msg.type) {
case '1':_login_ucnode(head, sd, &addr, &msg);break;
case '2':_broadcast_ucnode(head, sd, &addr, &msg);break;
case '3':_quit_ucnode(head, sd, &addr, &msg);break;
default://未識別的異常報文,程序把其踢走
fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port), msg.type, msg.name, msg.text);
_quit_ucnode(head, sd, &addr, &msg);
break;
}
}
// 這段代碼是不會執行到這的, 可以加一些控制讓其走到這. 看人
close(sd);
_destroy_ucnode(&head);
return 0;
}
這裡主要圍繞的結構就是
// 維護一個客戶端鏈表信息,記錄登錄信息
typedef struct ucnode {
struct sockaddr_in addr;
struct ucnode* next;
} *ucnode_t ;
注冊添加登錄廣播退出等.這裡再扯一下. 關於C static開發技巧. C中有一種 *.h 開發模式, 全部采用static 內嵌代碼段. 這樣
可以省略*.c 文件. 小巧的封裝可以使用. 繼續扯一點. 開發也寫C++,雖然鄙視. C++ 中有個 *.hpp文件. 比較好. 它表達的意思
是這個代碼是開源的. 全部采用充血模型. 類中代碼都放在類中實現.非常值得提倡. 這也是學boost的時候學到的. 很實在.
好了說代碼吧. 也比較隨大流. 看看也都明白了. 簡單分析一處吧
// 這裡是有用戶登錄處理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
_insert_ucnode(head, pa);
head = head->next;
// 從此之後才為以前的鏈表
while(head->next){
head = head->next;
IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
}
}
因為我采用的頭查法. 那就除了剛插入的頭的下一個結點都需要發送登錄信息. 比較精巧.
好看編譯命令
gcc -g -Wall -o udpmulsrv.out udpmulsrv.c gcc -g -Wall -o udpmulclt.out udpmulclt.c
最後測試截圖如下

很好玩,歡迎嘗試.到這裡基本上udp基礎api 應該都了解了.從上面代碼也許能看出來. 設計比較重要. 設計決定大思路.
下次有機會 要麼分享開源的網絡庫,要麼分享數據庫開發.
後記
錯誤是難免的,歡迎吐槽交流. ( ^_^ )/~~拜拜
別董大(其一) 高適 千裡黃雲白日曛, 北風吹雁雪紛紛。 莫愁前路無知己, 天下誰人不識君。 (作者注: 別董大意思是 分別了我的朋友董大)