Linux - socket3

①、無鏈接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;
}
複製代碼
相關文章
相關標籤/搜索