Linux網絡套接字編程(socket詳解)

前言

套接字編程其實就是網絡編程,套接字實際就是一套網絡通信程序編寫的接口,通過這些接口,並且提供相關信息,就可以實現傳輸層以下幾層的操作。

網絡通信中涉及兩臺主機之間的通信:客戶端(主動發送請求)、服務端(被動接收請求)。

一:TCP/UDP協議的基本認識

在TCP/IP網絡體系結構中,TCP協議和UDP協議是傳輸層兩種典型的協議,爲上層用戶提供級別的通信可靠性。

1.1 TCP:傳輸控制協議(Transport Control Protocol)

傳輸特點: 有連接、可靠傳輸、面向字節流

TCP通信需要建立連接(打電話),確保數據被對方收到,有序的安全的字節流傳輸服務

應用場景: 數據傳輸安全性要求高(文件傳輸)

1.2 UDP:用戶數據報協議(User Data Protocol)

傳輸特點: 無連接、不可靠、面向數據報

UDP通信不需要建立連接(發短信),不確保數據是否被對方收到,無序的不可靠的數據塊傳輸

應用場景: 數據傳輸實時性要求高(視頻傳輸 )

二:UDP通信流程

在這裏插入圖片描述
注意:客戶端用哪個源端地址信息發送數據不影響大局,只要服務端會回覆到客戶端綁定的源端地址信息就可以。(客戶端一旦自己綁定源端地址信息,若選用的端口已經被佔用則會綁定失敗,所以不推薦客戶端主動綁定源端地址信息)

2.1 UDP套接字相關接口

  1. 創建套接字

int socket(int domain, int type, int protocal) 返回一個非負整數(找到socket結構體)

domain:地址域

不同的協議版本有不同的地址結構——IP地址結構:IPv4(AF_INET)、IPv6(AF_INET6)

確定socket通信使用哪種協議版本的地址結構

type:套接字類型

數據報套接字: SOCK_DGRAM提供數據報傳輸服務(無連接、不可靠、有最大長度限制的消息傳輸服務)

流式套接字: SOCK_STREAM提供字節流傳輸服務 (有序、可靠、基於連接的消息傳輸服務)

protocal:協議類型

數據報套接字:默認UDP協議(IPPROTO_UDP)

流式套接字:默認TCP協議(IPPROTO_TCP)

  1. 爲套接字綁定地址信息

int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)

sockfd:創建套接字返回的描述符

addr: 地址信息的結構(綁定各種各樣的地址信息)

bind可以綁定不同的地址結構,爲了實現接口統一,用戶定義時定義自己需要的地址結構,綁定時統一將類型強轉爲sockaddr*
在這裏插入圖片描述

addrlen: 地址信息的長度

  1. 接收數據

int recvfrom(int sockfd, char* buf, int buf_len, int flag, struct sockaddr* peer_addr, socklen_t* addr_len)

sockfd:指定內核中的socket結構體(從哪個socket的接收緩衝區中取出數據)

buf:用戶態緩衝區,存放從接收緩衝區中取出的數據

buflen:想要獲取的數據長度

flag:操作選項,默認爲0阻塞接收(緩衝區中沒有數據則阻塞等待)

peer_addr:地址緩衝區首地址,獲取發送這個數據的源端地址信息

addr_len:指定想要獲取地址信息的長度以及返回實際獲取的長度

  1. 發送數據

ssize_t sendto(int sockfd, char* data, int data_len, int flag, struct sockaddr* addr dest_addr, socklen_t addrlen)

sockfd:指定內核中socket結構體,綁定的地址信息做爲數據中的源信息對數據進行描述

data/datalen:要發送的數據以及數據長度

flag:默認爲0阻塞發送數據,若發送緩衝區中數據飽和則進行等待

dest_addr:目的端的地址信息

addr_len:地址信息長度

  1. 關閉套接字

int close(int fd)

2.2 UDP套接字代碼實現

  • 主機字節序到網絡字節序的轉換接口(端口):

32位整數:uint32_t htonl(uint32_t hostlong)
16位整數:uint16_t htons(uint16_t hostshort)

  • 網絡字節序到主機字節序的轉換接口(端口):

32位整數:uint32_t ntohl(uint32_t netlong)
16位整數:uint16_t ntohs(uint16_t netshort)

  • 將字符串的點分十進制IP地址轉換爲網絡字節序的整數IP地址:

int_addr_t inet_addr(const char* cp)

  • 將網絡字節序的整數IP地址轉換爲字符串的點分十進制IP地址:

char* inet_ntoa(struct in_addr in)

  • 將字符串的IP地址轉換爲網絡字節序的整數IP地址:

int inet_pton(int af, const char* src, void* dst)

  • 將網絡字節序的整數IP地址轉化爲字符串的IP地址:

const char* inet_ntop(int af, const void* src, char* dst, socklen_t size)

UDPsocket.hpp:C++封裝一個UDPsocket類

// 使用C++封裝一個UDPsocket類
// 實例化出的每一個對象都是一個UDP通信套接字
// 並且通過成員函數實現UDP通信流程

#include<cstdio>
#include<string>
#include<sys/socket.h> // 套接字接口信息
#include<netinet/in.h> // 包含地址結構
#include<arpa/inet.h>
#include<unistd.h>

using namespace std;

class UDPsocket{
public:
    // 構造函數
    UDPsocket()
        :_sockfd(-1)
    {}
    // 1.創建套接字
    bool Socket(){
        _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if(_sockfd < 0){
            perror("socket error");
            return false;
        }
        return true;
    }
    
    // 2.爲套接字綁定地址信息
    bool Bind(const string& ip, uint16_t port){
       // 定義IPV4地址結構
       struct sockaddr_in addr;
       addr.sin_family = AF_INET;
       // 將主機字節序短整型轉換爲網絡字節序短整型
       addr.sin_port = htons(port);
       // 將字符串ip地址轉換爲網絡字節序ip地址
       addr.sin_addr.s_addr = inet_addr(ip.c_str());
       // 綁定
       socklen_t len = sizeof(struct sockaddr_in);
       int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
       if(ret < 0){
           perror("bind error");
           return false;
       }
       return true;
    }

    // 3.接收數據並且獲取發送端的地址信息
    bool Recv(string* buf, string* ip = NULL, uint16_t* port = NULL){
        struct sockaddr_in peer_addr;
        socklen_t len = sizeof(struct sockaddr_in);
        char tmp[4096] = {0};
        int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
        if(ret < 0){
            perror("recv error");
            return false;
        }
        // 從tmp中截取ret個字節放到buf中
        buf->assign(tmp, ret);
        if(port != NULL){
            // 網絡字節序轉爲主機字節序
            *port = ntohs(peer_addr.sin_port);
        }
        if(ip != NULL){
            // 網絡字節序到字符串IP地址的轉換
            *ip = inet_ntoa(peer_addr.sin_addr);
        }
        return true;
    }

    // 4.發送數據
    bool Send(const string& data, const string& ip, const uint16_t port){
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
        if(ret < 0){
            perror("send error");
            return false;
        }
        return true;
    }

    // 5.關閉套接字
    bool Close(){
        if(_sockfd > 0){
            close(_sockfd);
            _sockfd = -1;
        }
        return true;
    }
private:
    // UDP通信套接字描述符
    int _sockfd;
};

UDPsrv.cc:UDP服務端

#include<iostream>
#include<string>
#include"UDPsocket.hpp"
using namespace std;
#define CHECKRET(q) if((q)==false){return -1;}
int main(int argc, char* argv[]){
    // argc表示程序運行參數的個數
    if(argc != 3){
        cout << "Usage:./UDPsrv IP Port" << endl;
        return -1;
    }
    uint16_t port = stoi(argv[2]);
    string ip = argv[1];

    UDPsocket srvsock;
    // 1.創建套接字
    CHECKRET(srvsock.Socket());
    // 2.爲套接字綁定地址信息
    CHECKRET(srvsock.Bind(ip, port));
    
    while(1){
        // 3.接收數據
        string buf;
        string peer_ip;
        uint16_t peer_port;
        CHECKRET(srvsock.Recv(&buf, &peer_ip, &peer_port));
        cout << "client[" << peer_ip << ":" << peer_port << "]say:" << buf << endl;

        // 4.發送數據
        buf.clear();
        cout << "server say: ";
        cin >> buf;
        CHECKRET(srvsock.Send(buf, peer_ip, peer_port));
    }

    // 5.關閉套接字
    srvsock.Close();
    return 0;
}

UDPcli.cc:UDP客戶端

#include<iostream>
#include<string>
#include"UDPsocket.hpp"
using namespace std;
#define CHECKRET(q) if((q) == false){return -1;}

int main(int argc, char* argv[]){
    // 客戶端獲取的IP地址是服務端綁定的,也就是客戶端發送的目標地址
    if(argc != 3){
        cout << "Usage: ./UDPcli ip port" << endl;
        return -1;
    }
    string srv_ip = argv[1];
    uint16_t srv_port = stoi(argv[2]);

    UDPsocket clisock;
    // 1.創建套接字
    CHECKRET(clisock.Socket());
    // 2.爲套接字綁定地址信息(不推薦主動綁定)
    while(1){
        // 3.發送數據
        cout << "client say:";
        string buf;
        cin >> buf;
        CHECKRET(clisock.Send(buf, srv_ip, srv_port));
        // 4.接收數據
        buf.clear();
        CHECKRET(clisock.Recv(&buf));
        cout << "server say: " << buf << endl;
    }

    // 5.關閉套接字
    clisock.Close();
    return 0;
}

三:TCP通信流程

在這裏插入圖片描述
注意:TCP通信需要建立連接,監聽套接字在收到客戶端的連接請求後,纔會創建通信套接字用於指定客戶端和服務端的通信,通信套接字同時包含源端地址信息和對端地址信息,收發數據沒有固定的先後順序。

3.1 TCP套接字相關接口

  1. 創建套接字

接口與UDP通信相同,SOCK_STREAM爲流式套接字,默認TCP協議

  1. 爲套接字綁定地址信息

接口與UDP通信相同

  1. 開始監聽

listen(int sockfd, int backlog)

sockfd:套接字描述符(設置此套接字爲監聽狀態,並且開始接收客戶端的連接請求)

backlog:同一時間的併發連接數

  1. 獲取新建連接

從已完成連接的套接字隊列中取出一個socket,並且返回這個socket的描述符

int accept(int sockfd, struct sockaddr* cli_addr, socklen_t len)

sockfd:監聽套接字描述符(獲取哪個服務端套接字的新建連接)

cli_addr / len:新建套接字對應的客戶端地址信息以及地址信息長度

返回值:新建套接字的套接字描述符

  1. 收發數據

TCP通信套接字中已經包含了源端地址信息和對端地址信息,所以在接收數據的時候不需要獲取對方的地址信息,發送數據的時候也不需要指定對端地址信息。

ssize_t recv(int sockfd, char* buf, int len, int flag):默認阻塞,緩衝區沒有數據則等待,連接斷開返回0
ssize_t send(int sockfd, char* data, int len, int flag):默認阻塞,緩衝區滿則等待,連接斷開觸發SIGPIPE異常

  1. 關閉套接字

close(fd)

  1. 客戶端向服務端發起連接請求

int connect(int sockfd, struct sockaddr* srv_addr, int len)

srv_addr:服務端地址信息

connect也會在套接字socket中描述對端地址信息

3.2 TCP套接字代碼實現

TCPsocket.hpp:C++封裝一個TCPsocket類

#include<cstdio>
#include<string>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
#define MAX_LISTEN 5
class TCPsocket{
public:
    // 構造函數
    TCPsocket()
        :_sockfd(-1)
    {}

    // 1.創建套接字
    bool Socket(){
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(_sockfd < 0){
            perror("socket error");
            return false;
        }
        return true;
    }

    // 2.爲套接字綁定地址信息
    bool Bind(const string& ip, uint16_t port){
        // 組織地址結構IPV4
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
        if(ret < 0){
            perror("bind error");
            return false;
        }
        return true;
    }

    // 3.開始監聽
    bool Listen(int backlog = MAX_LISTEN){
        int ret = listen(_sockfd, backlog);
        if(ret < 0){
            perror("listen error");
            return false;
        }
        return true;
    }

    // 4.獲取新建連接
    bool Accept(TCPsocket* new_sock, string* ip = NULL, uint16_t* port = NULL){
        struct sockaddr_in addr;
        socklen_t len = sizeof(struct sockaddr_in);
        int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
        if(new_fd < 0){
            perror("accept error");
            return false;
        }
        new_sock->_sockfd = new_fd;
        if(ip != NULL){
            *ip = inet_ntoa(addr.sin_addr);
        }
        if(port != NULL){
            *port = ntohs(addr.sin_port);
        }
        return true;
    }

    // 5.接收數據
    bool Recv(string* buf){
        char tmp[4096] = {0};
        int ret = recv(_sockfd, tmp, 4096, 0);
        if(ret < 0){
            perror("recv error");
            return false;
        }
        else if(ret == 0){
            printf("disconnected\n");
            return false;
        }
        
        buf->assign(tmp, ret);
        return true;
    }

    // 6.發送數據
    bool Send(const string& data){
        int ret = send(_sockfd, data.c_str(), data.size(), 0);
        if(ret < 0){
            perror("send error");
            return false;
        }
        return true;
    }

    // 7.關閉套接字
    bool Close(){
        if(_sockfd > 0){
            close(_sockfd);
            _sockfd = -1;
        }
        return true;
    }

    // 8.發送連接請求
    bool Connect(const string& ip, uint16_t port){
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
        if(ret < 0){
            perror("connect error");
            return false;
        }
        return true;
    } 
        
private:
    int _sockfd;
};

TCPsrv.cc:TCP服務端

#include<iostream>
using namespace std;
#include"TCPsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Usage: ./TCPsrv ip port" << endl;
        return -1;
    }

    string ip = argv[1];
    uint16_t port = stoi(argv[2]);

    // 創建套接字
    TCPsocket listen_sock;
    CHECK_RET(listen_sock.Socket());

    // 綁定地址信息
    CHECK_RET(listen_sock.Bind(ip, port));

    // 開始監聽
    CHECK_RET(listen_sock.Listen());

    while(1){
        TCPsocket new_sock;
        bool ret = listen_sock.Accept(&new_sock);
        if(ret == false){
            // 服務端不能因爲獲取一個新建套接字失敗就退出
            continue;
        }
        string buf;
        new_sock.Recv(&buf);
        cout << "client say: " << buf << endl;

        buf.clear();

        cout << "server say: " << endl;
        cin >> buf;
        new_sock.Send(buf);
    }

    listen_sock.Close();
    return 0;
}

TCPcli.cc:TCP客戶端

#include<iostream>
using namespace std;
#include"TCPsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Usage: ./TCPcli ip port" << endl;
        return -1;
    }

    string ip = argv[1];
    uint16_t port = stoi(argv[2]);

    TCPsocket sock;
    CHECK_RET(sock.Socket());
    CHECK_RET(sock.Connect(ip, port));

    while(1){
        string buf;
        cout << "client say: " << endl;
        cin >> buf;
        sock.Send(buf);

        buf.clear();

        sock.Recv(&buf);
        cout << "server say: " << buf << endl;

    }
    sock.Close();
    return 0;
}

我們很容易發現一個問題:

本次實現的TCP通信流程只能完成一個客戶端與服務端的一次通信,因爲服務端的開始監聽和收發數據放在同一個while死循環中,數據一次的收發結束,就會回到監聽階段,阻塞等待新連接的到來,從而只能完成一個客戶端與服務端的一次通信。

解決辦法:

父進程用於監聽操作,阻塞等待新連接的到來

子進程用於指定客戶端與服務端的通信,父子進程互不影響。

注意:

子進程退出時,父進程需要循環非阻塞等待子進程的退出,避免產生殭屍進程。

並且這裏還需要處理SIGCHILD信號(不可靠信號)爲自定義,直到有子進程退出時才循環非阻塞處理,在一次處理中必須處理到沒有子進程退出纔可以,避免信號的丟失。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章