Linux网络编程 | Socket编程(一):Socket的介绍、UDPSocket的封装、UDP服务器/客户端的实现
目錄
- 套接字編程
- Sockaddr結(jié)構(gòu)
- 字節(jié)序
- 地址轉(zhuǎn)換
- 常用套接字接口
- UDP的通信流程
- UDPSocket的封裝
- UDP服務器
- UDP客戶端
套接字編程
所謂套接字(Socket),就是對網(wǎng)絡中不同主機上的應用進程之間進行雙向通信的端點的抽象。一個套接字就是網(wǎng)絡上進程通信的一端,提供了應用層進程利用網(wǎng)絡協(xié)議交換數(shù)據(jù)的機制。
Sockaddr結(jié)構(gòu)
在linux下,根據(jù)所使用的不同協(xié)議,又分為以下三種結(jié)構(gòu),在使用時,我們可以選擇自己所需要的結(jié)構(gòu),通信時再將我們所使用的結(jié)構(gòu)強轉(zhuǎn)為sockaddr,這樣就能保證數(shù)據(jù)格式的一致
- IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結(jié)構(gòu)體表示,包括16位地址類型, 16 位端口號和32位IP地址.
- IPv4、IPv6地址類型分別定義為常數(shù)AF_INET、AF_INET6. 這樣,只要取得某種sockaddr結(jié)構(gòu)體的首地址, 不需要知道具體是哪種類型的sockaddr結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容.
- socket API可以都用struct sockaddr *類型表示, 在使用的時候需要強制轉(zhuǎn)化成sockaddr_in; 這樣的好 處是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各種類型的sockaddr結(jié)構(gòu)體指針做為 參數(shù);
通常情況下,因為我們使用的都是IPV4,所以一般用的都是sockaddr_in結(jié)構(gòu),這個結(jié)構(gòu)中用來描述通信雙方的主要信息就是端口號和ip地址
sockaddr_in結(jié)構(gòu)
sin_family:指代協(xié)議族,在socket編程中只能是AF_INET
sin_port:存儲端口號(使用網(wǎng)絡字節(jié)順序)
sin_addr:存儲IP地址,使用in_addr這個數(shù)據(jù)結(jié)構(gòu)
sin_zero:是為了讓sockaddr與sockaddr_in兩個數(shù)據(jù)結(jié)構(gòu)保持大小相同而保留的空字節(jié)。
in_addr結(jié)構(gòu)
這個結(jié)構(gòu)中存儲的其實就是一個32位的整型ip地址。
字節(jié)序
字節(jié)序就是CPU對數(shù)據(jù)再內(nèi)存中以字節(jié)為單位的存取順序,也就是我們通常所說的大端小端問題。
關于大小端的問題我之前有寫過一篇博客
大端小端存儲解析
這里就簡要說一下
大端存儲模式:是指數(shù)據(jù)的低位保存在內(nèi)存的高地址中,而數(shù)據(jù)的高位,保存在內(nèi)存的低地址中。
小端存儲模式:是指數(shù)據(jù)的低位保存在內(nèi)存的低地址中,而數(shù)據(jù)的高位,保存在內(nèi)存的高地址中。
在網(wǎng)絡通信中,網(wǎng)絡字節(jié)序采用大端的存儲模式,而主機字節(jié)序根據(jù)主機不同也不一樣,我們現(xiàn)在的家用機一般都是小端,但網(wǎng)絡上的通信不能確保主機字節(jié)序的唯一性,因為受眾是整個網(wǎng)絡,而一旦通信的雙方主機字節(jié)序不同,就會造成通信時的數(shù)據(jù)二義,所以需要確保字節(jié)序相同,就需要在通信時將主機字節(jié)序轉(zhuǎn)換為通用的網(wǎng)絡字節(jié)序。
在arpa/inet.h這個頭文件中,也為我們提供了一套字節(jié)序的轉(zhuǎn)換接口。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);//將無符號長整型的主機字節(jié)序轉(zhuǎn)換為網(wǎng)絡字節(jié)序uint16_t htons(uint16_t hostshort);//將無符號短整型的主機字節(jié)序轉(zhuǎn)換為網(wǎng)絡字節(jié)序uint32_t ntohl(uint32_t netlong);//將無符號長整型的網(wǎng)絡字節(jié)序轉(zhuǎn)換為主機字節(jié)序uint16_t ntohs(uint16_t netshort);//將無符號短整型的網(wǎng)絡字節(jié)序轉(zhuǎn)換為主機字節(jié)序//h代表主機字節(jié)序,n代表網(wǎng)絡字節(jié)序,l代表長整型,s代表短整型地址轉(zhuǎn)換
同樣的,我們輸入進去的ip地址一般都是點分十進制的ip地址,而通信時需要的是網(wǎng)絡字節(jié)序的整數(shù)ip地址。
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);//將網(wǎng)絡字節(jié)序的整數(shù)ip地址轉(zhuǎn)換為點分十進制的字符串ip地址in_addr_t inet_addr(const char *cp);//將點分十進制的字符串ip地址轉(zhuǎn)換為網(wǎng)絡字節(jié)序的整數(shù)ip地址int inet_aton(const char *cp, struct in_addr *inp);//將點分十進制的字符串ip地址轉(zhuǎn)換為網(wǎng)絡字節(jié)序的整數(shù)ip地址(與addr的區(qū)別它會認為如255.255.255.255這類特殊地址有效)in_addr_t inet_network(const char *cp); //將點分十進制的字符串ip地址轉(zhuǎn)換為主機字節(jié)序的整數(shù)ip地址struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);in_addr_t inet_lnaof(struct in_addr in);in_addr_t inet_netof(struct in_addr in);常用套接字接口
// 創(chuàng)建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務器) int socket(int domain, int type, int protocol);// 綁定端口號 (TCP/UDP, 服務器) int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 開始監(jiān)聽socket (TCP, 服務器) int listen(int socket, int backlog);// 接收請求 (TCP, 服務器) int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立連接 (TCP, 客戶端) int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//斷開連接 int close(int sockfd); //發(fā)送數(shù)據(jù) ssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);//接收數(shù)據(jù) ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);UDP的通信流程
計算機網(wǎng)絡 (三) 傳輸層 :一文搞懂UDP與TCP協(xié)議
在這篇博客中,我描述了UDP與TCP的特性以及通信流程,下面就根據(jù)特性來規(guī)劃該如何通過Socket來實現(xiàn)UDP通信。
這里我就簡單的畫一個圖。
因為UDP是無連接的,所以只需要再創(chuàng)建套接字后綁定地址信息,就可以直接進行通信。
這里有一點需要注意,就是客戶端一般不會主動綁定地址信息。
原因是客戶端用什么地址和端口接收數(shù)據(jù)都無所謂,只需要確保能夠?qū)?shù)據(jù)發(fā)送出去即可。如果不綁定地址信息,系統(tǒng)會自動選擇合適的地址端口進行綁定,而如果手動綁定,很可能會綁定到已使用或者將要使用的端口,此時就會產(chǎn)生端口的沖突,所以為了減少端口沖突,客戶端一般不會主動綁定。
UDPSocket的封裝
為了能夠更方便的使用,一般都會根據(jù)不同協(xié)議和使用情景,來封裝一套Socket接口,使用時就只需要根據(jù)協(xié)議特性來傳遞參數(shù)即可。
具體的思路我都寫在了注釋里面
#include<iostream> #include<sys/socket.h> #include<string> #include<arpa/inet.h> #include<netinet/in.h> #include<cstdio> #include<unistd.h>//內(nèi)聯(lián)函數(shù),用來檢測當前操作是否出錯 inline void CheckSafe(bool ret) {if(ret == false){std::cerr << "Socket發(fā)生錯誤" << std::endl;exit(0);} }class UdpSocket {public:UdpSocket() : _socket_fd(-1){}//創(chuàng)建socketbool Socket(){_socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);if(_socket_fd == -1){perror("socket create error");return false;}return true;}//綁定地址信息bool Bind(const std::string& ip, uint16_t port){struct sockaddr_in addr;addr.sin_family = AF_INET;//主機字節(jié)序轉(zhuǎn)換成網(wǎng)絡字節(jié)序,方便統(tǒng)一addr.sin_port = htons(port);//將字符串的ip地址轉(zhuǎn)為網(wǎng)絡字節(jié)序的二進制數(shù)據(jù)addr.sin_addr.s_addr = inet_addr(ip.c_str());socklen_t len = sizeof(struct sockaddr_in);//強轉(zhuǎn)地址結(jié)構(gòu),使接口統(tǒng)一int ret = bind(_socket_fd, (struct sockaddr*)& addr, len);if(ret == -1){perror("socket bind error");return false;}return true;}bool Recv(std::string& buff, std::string* ip = NULL, uint16_t* port = NULL){//對端地址信息struct sockaddr_in peer_addr;socklen_t len = sizeof(struct sockaddr_in);//接收緩沖區(qū)char temp[1024] = {0};int ret = recvfrom(_socket_fd, temp, 1024, 0, (struct sockaddr*)&peer_addr, &len);if(ret == -1){perror("receive error");return false;}//將數(shù)據(jù)從緩沖區(qū)取出buff.assign(temp, ret);//獲取對端地址信息if(port != NULL){*port = htons(peer_addr.sin_port);}if(ip != NULL){*ip = inet_ntoa(peer_addr.sin_addr);}return true;}bool Send(const std::string& data, const std::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(_socket_fd, data.c_str(), data.size(), 0, (sockaddr*)& addr, len);if(ret == -1){perror("send error");return false;}return true;}void Close(){close(_socket_fd);_socket_fd = -1;}private:int _socket_fd; };
按照前面所化的流程以及封裝的對應接口,來實現(xiàn)服務端
UDP服務器
#include<iostream> #include"UdpSocket.hpp"using namespace std;int main (int argc, char *argv[]) {if(argc != 3){cerr << "正確輸入方式: ./udp_srv.cpp ip port\n" << endl;return -1;}//獲取命令行輸入的ip地址和端口string ip = argv[1];uint16_t port = stoi(argv[2]);UdpSocket Socket;//創(chuàng)建套接字CheckSafe(Socket.Socket());//綁定地址信息CheckSafe(Socket.Bind(ip, port));while(1){string cli_ip;uint16_t cli_port;string message;//接受數(shù)據(jù)CheckSafe(Socket.Recv(message, &cli_ip, &cli_port)); cout << "cli[" << cli_ip << ":" << cli_port << "]:send message: " << message << endl;message.clear();cout << "srv send reply message: "; getline(cin, message);//給客戶端回復數(shù)據(jù)CheckSafe(Socket.Send(message, cli_ip, cli_port));}//關閉套接字Socket.Close();return 0; }
按照前面所化的流程以及封裝的對應接口,來實現(xiàn)客戶端
UDP客戶端
#include<iostream> #include"UdpSocket.hpp"using namespace std;int main (int argc, char *argv[]) {if(argc != 3){cerr << "正確輸入方式: ./udp_cli.cpp ip port\n" << endl;return -1;}//獲取命令行輸入的ip地址和端口string ip = argv[1];uint16_t port = stoi(argv[2]);UdpSocket Socket;//創(chuàng)建套接字CheckSafe(Socket.Socket());//發(fā)送方不需要主動綁定地址信息,讓系統(tǒng)自動選取即可,因為只需要保證能夠發(fā)送數(shù)據(jù),并且接收到數(shù)據(jù)即可,哪個地址端口都無所謂,這樣還能減少端口沖突的概率//發(fā)送數(shù)據(jù)while(1){cout << "cli send message: ";string message;getline(cin, message);//如果輸入quit則退出if(message == "quit")break;//發(fā)送數(shù)據(jù)CheckSafe(Socket.Send(message, ip, port));message.clear();//接受數(shù)據(jù)CheckSafe(Socket.Recv(message));cout << "srv reply message: " << message << endl;}//關閉套接字Socket.Close();return 0; }服務端
客戶端
總結(jié)
以上是生活随笔為你收集整理的Linux网络编程 | Socket编程(一):Socket的介绍、UDPSocket的封装、UDP服务器/客户端的实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 海量数据处理(一) :位图与布隆过滤器的
- 下一篇: Linux网络编程 | Socket编程