2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto
文章目錄
- 一:回顧
- 二:徹底了解套接字和struct socket結(jié)構(gòu)
- (1)一切皆文件-文件描述符-套接字描述符
- (2)struct socket結(jié)構(gòu)
- A:struct socket結(jié)構(gòu)體作用
- B:struct socket結(jié)構(gòu)體詳解
- 三:socket接口(UDP)和sockaddr結(jié)構(gòu)
- (1)socket常用API接口1(UDP)
- (2)sockaddr結(jié)構(gòu)
- A:struct sockaddr結(jié)構(gòu)
- B:struct sockaddr_in
- 四:UDP通信示例
- (1)UDP通信
- (1)sendto和recvfrom接口
- (2)代碼
- (3)效果
一:回顧
前文講過,套接字分為流式套接字(SOCKET_STREAM)和數(shù)據(jù)報(bào)套接字(SOCKET_DGRAM),他們所采用的協(xié)議分別為TCP和UDP,相應(yīng)的對(duì)應(yīng)的Socket編程就是TCP套接字編程和UDP套接字編程
相比于UDP而言,TCP保證了數(shù)據(jù)的可靠傳輸,所以它比UDP就復(fù)雜一點(diǎn),從下面的流程圖中也可以看出來
- 流式套接字
- 數(shù)據(jù)包套接字
二:徹底了解套接字和struct socket結(jié)構(gòu)
(1)一切皆文件-文件描述符-套接字描述符
下圖是Linux內(nèi)核中關(guān)于socket的數(shù)據(jù)結(jié)構(gòu)還有后續(xù)我們?cè)僬f編程時(shí)的一些API接口。
你可能注意到了一個(gè)非常熟悉的地方,struct file* file,這不就是文件嗎?是的沒錯(cuò),如果再深入理解一點(diǎn),其實(shí)套接字就是使用文件描述符和其它程序進(jìn)行通訊的一種方式。我們知道,Linux系統(tǒng)在執(zhí)行任何I/O的時(shí)候,都在和文件描述符打交道,而在Linux下,我們一直反復(fù)強(qiáng)調(diào)“一切皆文件的思想”,之前說過的屏幕都可以作為文件,那么現(xiàn)在接觸的網(wǎng)卡也當(dāng)然可以做文件
所以以后再進(jìn)行網(wǎng)絡(luò)通訊時(shí),你會(huì)利用socket系統(tǒng)調(diào)用,它將返回套接字文件描述符,然后你會(huì)利用它再通過相應(yīng)(如下)接口進(jìn)行通信操作。
既然它是文件描述符,那就意味著你仍然可以使用read()和write()來進(jìn)行通信,但是“術(shù)業(yè)有專攻”,網(wǎng)絡(luò)的事情還是盡量使用它們對(duì)應(yīng)的接口來操作。
(2)struct socket結(jié)構(gòu)
A:struct socket結(jié)構(gòu)體作用
用戶使用socket系統(tǒng)調(diào)用編寫程序時(shí),通過套接字描述符完成相關(guān)操作
int socket(int domain, int type, int protocol);它對(duì)應(yīng)的就是我們?cè)谏厦嬲f到的struct socket結(jié)構(gòu)體
那么內(nèi)核中為什么要有struct socket這樣的結(jié)構(gòu)體呢,以及它有什么作用呢?可以看下面這張圖
所以內(nèi)核中的進(jìn)程可以通過該結(jié)構(gòu)體來訪問Linux內(nèi)核中的傳輸層,網(wǎng)絡(luò)層和數(shù)據(jù)鏈路層,也就是說struct socket是內(nèi)核中的進(jìn)程與內(nèi)核中的網(wǎng)絡(luò)系統(tǒng)的橋梁
B:struct socket結(jié)構(gòu)體詳解
這是一個(gè)基本的BSD socket,我們調(diào)用socket系統(tǒng)調(diào)用創(chuàng)建的各種不同類型的socket,開始創(chuàng)建的都是它,到后面**,各種不同類型的socket在它的基礎(chǔ)上進(jìn)行 各種擴(kuò)展。struct socket是在虛擬文件系統(tǒng)上被創(chuàng)建出來的,可以把它看成一個(gè)文件,**是可以被安全地?cái)U(kuò)展的。下面是其完整定義:
struct socket { socket_state state; unsigned long flags; const struct proto_ops *ops; struct fasync_struct *fasync_list; struct file *file; struct sock *sk; wait_queue_head_t wait; short type; };1: state用于表示socket所處的狀態(tài),是一個(gè)枚舉變量,其類型定義如下:
該成員只對(duì)TCP socket有用,因?yàn)橹挥衪cp是面向連接的協(xié)議,udp跟raw不需要維護(hù)socket狀態(tài)。
typedef enum { SS_FREE = 0, //該socket還未分配 SS_UNCONNECTED, //未連向任何socket SS_CONNECTING, //正在連接過程中 SS_CONNECTED, //已連向一個(gè)socket SS_DISCONNECTING //正在斷開連接的過程中 }socket_state;2:ops是協(xié)議相關(guān)的一組操作集,結(jié)構(gòu)體struct proto_ops的定義如下:
struct proto_ops { int family; struct module *owner; int (*release)(struct socket *sock); int (*bind)(struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect)(struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept)(struct socket *sock,struct socket *newsock, int flags); int (*getname)(struct socket *sock, struct sockaddr *addr,int *sockaddr_len, int peer); unsigned int (*poll)(struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl)(struct socket *sock, unsigned int cmd, unsigned long arg); int (*listen)(struct socket *sock, int len); int (*shutdown)(struct socket *sock, int flags); int (*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, int optlen); int (*getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); int (*sendmsg)(struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len); int (*recvmsg)(struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len, int flags); int (*mmap)(struct file *file, struct socket *sock,struct vm_area_struct * vma); ssize_t (*sendpage)(struct socket *sock, struct page *page, int offset, size_t size, int flags); };其中協(xié)議棧總共定義了了三個(gè)strcut proto_ops類型的變量,分別myinet_stream_ops, myinet_dgram_ops, myinet_sockraw_ops,分別對(duì)應(yīng)流式套接字,數(shù)據(jù)報(bào)套接字和原生套接字
3:type是socket的類型,對(duì)應(yīng)的取值如下:
enum sock_type { SOCK_DGRAM = 1, //數(shù)據(jù)報(bào)套接字SOCK_STREAM = 2, //流式套接字SOCK_RAW = 3, //原生套接字SOCK_RDM = 4, SOCK_SEQPACKET = 5, SOCK_DCCP = 6, SOCK_PACKET = 10, };4:sk是網(wǎng)絡(luò)層對(duì)于socket的表示,用戶需要進(jìn)行傳參,讓網(wǎng)絡(luò)層采用TCP還是UDP
三:socket接口(UDP)和sockaddr結(jié)構(gòu)
(1)socket常用API接口1(UDP)
所以我們要完成通訊,就要利用sockt結(jié)構(gòu)體提供給我們的一些接口來實(shí)現(xiàn)。由于UDP只負(fù)責(zé)傳送,所以相較于TCP而言它的接口少一點(diǎn),所以這里只列出UDP中使用到的,但需要注意的是下面的接口對(duì)TCP也是通用的,只不過TCP相較于UDP的接口要多一點(diǎn),還要擴(kuò)展一點(diǎn)。
本節(jié)的模型就是一個(gè)服務(wù)端接受客戶端發(fā)送的消息,然后回應(yīng)客戶端
1:創(chuàng)建 一個(gè)socket 文件描述符
#include <sys/tyeps.h> #include <sys/socket.h> int socket(int domain,int type,int protocol);他們?nèi)齻€(gè)參數(shù)的含義及選用如下
對(duì)于它的返回值其實(shí)前面我們已經(jīng)說過了,本質(zhì)是一個(gè)文件描述符
2:綁定地址信息
前文說過,IP地址+端口號(hào)唯一表示了全網(wǎng)的一個(gè)進(jìn)程。服務(wù)端既然想要提供服務(wù),那么必須要讓客戶端知道怎么找到自己。所以對(duì)于服務(wù)端我們需要填入ip地址和端口號(hào),以便客戶端可以找到自己。 當(dāng)然客戶端一般是不要綁定的,當(dāng)數(shù)據(jù)返回給客戶端時(shí),具體要用哪一個(gè)端口,是操作系統(tǒng)決定的,如果我們?nèi)藶槿ソ壎?#xff0c;可能導(dǎo)致端口號(hào)的沖突
#include <sys/types.h> #include <sys/socket.h>int bind(int socket,const struct sockaddr* address,socklen_t addrss_len);第一個(gè)參數(shù)很好理解,第二個(gè)參數(shù)和第三個(gè)參數(shù)就是我們下面要說到的sockaddr結(jié)構(gòu)
(2)sockaddr結(jié)構(gòu)
所以在綁定時(shí),我們需要將ip和端口號(hào)一起封裝在某個(gè)結(jié)構(gòu)體中,然后傳參到bind接口里面。它共有三種類型的結(jié)構(gòu)體
你可能發(fā)現(xiàn)了,bind接口的形參給的是struct sockaddr*的指針,但是為什么這里有三種類型的結(jié)構(gòu)呢。其實(shí)這樣的設(shè)計(jì)主要是為了用更少的操作完成更多的事情。
在傳參時(shí),這三種結(jié)構(gòu)體的前16位是不相同的,它就是通過這個(gè)來區(qū)分的,所以只要保證在對(duì)齊的情況下,就能用struct sockaddr*來接受不同的結(jié)構(gòu),這有點(diǎn)像C++中的切片操作
A:struct sockaddr結(jié)構(gòu)
struct sockaddr為許多類型的套接字存儲(chǔ)套接字地址信息,其結(jié)構(gòu)如下
struct sockaddr {unsigned short sa_family; /* 地址家族, AF_xxx */char sa_data[14]; /*14字節(jié)協(xié)議地址*/ };這個(gè)結(jié)構(gòu)體是一個(gè)通用的地址信息結(jié)構(gòu),它并不是某一個(gè)具體的地址信息結(jié)構(gòu)。所以我們不選擇它,而選擇struct sockaddr_int這種結(jié)構(gòu)
B:struct sockaddr_in
該結(jié)構(gòu)體如下
struct sockaddr_in {short int sin_family; /* 通信類型 */unsigned short int sin_port; /* 端口 */struct in_addr sin_addr; /* Internet 地址 */unsigned char sin_zero[8]; /* 與sockaddr結(jié)構(gòu)的長(zhǎng)度相同*/};用這樣結(jié)構(gòu)可以很輕松的處理套接字地址的基本元素。
現(xiàn)在讓我們回到bind接口,看一下它是如何傳參的:當(dāng)我們使用ipv4版本的洗衣,綁定地址信息的時(shí)候,需要填充struct sockadd_in結(jié)構(gòu)體來保存服務(wù)器的ip和端口號(hào)
四:UDP通信示例
(1)UDP通信
UDP是面向非連接的協(xié)議,它不與對(duì)方建立連接,而是直接把數(shù)據(jù)報(bào)發(fā)給對(duì)方。UDP無需建立類如三次握手的連接,使得通信效率很高。因此UDP適用于一次傳輸數(shù)據(jù)量很少、對(duì)可靠性要求不高的或?qū)?shí)時(shí)性要求高的應(yīng)用場(chǎng)景。
UDP服務(wù)端流程如下
UDP客戶端流程如下
需要注意以下幾點(diǎn)
服務(wù)器和客戶端地址理應(yīng)是不一樣的,這里為了測(cè)試使用本地環(huán)回地址
ip地址實(shí)際是點(diǎn)分十進(jìn)制,每個(gè)部分算作1個(gè)字節(jié),但是我們輸入時(shí)往往是字符串,所以下面的接口可以將字符串轉(zhuǎn)換為正確的IP地址,并且是網(wǎng)絡(luò)字節(jié)序
如果需要將四字節(jié)序列轉(zhuǎn)為點(diǎn)分十進(jìn)制,則用char *inet_ntoa (struct in_addr);
本地環(huán)回:本地環(huán)回地址為127.0.0.1,這是一個(gè)測(cè)試IP。表示數(shù)據(jù)會(huì)完整的走一遍協(xié)議,但是是自己發(fā)自己收
對(duì)于服務(wù)端一般不指定某個(gè)ip,因?yàn)橛锌赡軙?huì)有很多個(gè)ip,如果指定了ip,服務(wù)端只能接受特定ip的數(shù)據(jù)。所以我們一般把ip設(shè)置為一個(gè)宏,也即INADDR_ANY表示綁定任意IP
(1)sendto和recvfrom接口
1:UDP發(fā)送函數(shù)
#include <sys/types.h> #include <sys/socket.h> int sendto( int sockfd,//套接字描述符 const void* buf,//要發(fā)什么東西 size_t len,//期望發(fā)多長(zhǎng) ,int flags,//阻塞還是非阻塞,一般設(shè)置為0 const struct sockaddr* dest_addr,//指向服務(wù)器的struct_in結(jié)構(gòu)體(注意強(qiáng)轉(zhuǎn)) socklen_t addrlen//指的是上面結(jié)構(gòu)體的長(zhǎng)度 )2:UDP接受函數(shù)
#include <sys/tyeps.h> #include <sys/socket.h> ssize_t recvfrom( int sockfd,//套接字描述符 void* buf,//讀取到放在哪? size_t len,//期望讀取多長(zhǎng) int flags.//沒有數(shù)據(jù)讀的時(shí)候掛起,默認(rèn)設(shè)置為0表示阻塞等待 struct sockaddr* src_addr, socklen_t* addrlen//保存那個(gè)客戶端發(fā)給你的(這兩個(gè)如果你不關(guān)心是誰發(fā)給你的,設(shè)置為null即可。)(2)代碼
udpServer.h
#include <iostream> #include <cstdio> #include <string> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std;class udpServer { private:int _port;//端口號(hào)int _sock;//套接字描述符public:udpServer(int port=8080):_port(port){}void initServer()//初始化服務(wù)器{_sock=socket(AF_INET,SOCK_DGRAM,0);//cout<<_sock<<endl;struct sockaddr_in local;//創(chuàng)建sockaddr_in結(jié)構(gòu)//填充local.sin_family=AF_INET;//IPV4協(xié)議local.sin_port=htons(_port);//主機(jī)字節(jié)序轉(zhuǎn)為網(wǎng)路字節(jié)序local.sin_addr.s_addr=INADDR_ANY;//綁定任意IP//綁定if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cerr << "綁定失敗" <<endl;exit(1);}}void startServer(){char msg[64];for(;;)//服務(wù)器永不停機(jī){msg[0]='\0';//清空緩沖區(qū)struct sockaddr_in end_point;//客戶端的信息socklen_t len=sizeof(end_point);//輸出和輸入型參數(shù),recvfrom和sendto都要用ssize_t ret=recvfrom(_sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);//接受,并把客戶端的信息保存在結(jié)構(gòu)體當(dāng)中if(ret > 0){char bu[16];sprintf(bu,"%d",ntohs(end_point.sin_port));//把網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)為主機(jī)字節(jié)序,端口號(hào)string cli=inet_ntoa(end_point.sin_addr);//把客戶端的ip轉(zhuǎn)為點(diǎn)分十進(jìn)制cli+=":";cli+=bu;msg[ret]='\0';cout<<"服務(wù)端接受到消息:來自->"<<cli<<" "<< msg << endl;string respond="服務(wù)端回消息";sendto(_sock,respond.c_str(),respond.size(),0,(struct sockaddr*)&end_point,len);//服務(wù)器應(yīng)答}}}~udpServer(){close(_sock);} };udpServer.cpp
#include "udpServer.h"int main(int argc,char* argv[]) {if(argc!=2)//判斷是否傳入端口號(hào){cout<<"端口號(hào)未傳入"<<endl;exit(1);}udpServer* ss=new udpServer(atoi(argv[1]));ss->initServer();ss->startServer(); }udpClient.h
#include <iostream> #include <string> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std;class udpClient { private:string _ip;//int _port;//客戶端要保存服務(wù)器的IP和端口int _sock;//套接字描述符public:udpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port){//連接服務(wù)器,本地環(huán)回測(cè)試}void initClient()//初始化服務(wù)器{_sock=socket(AF_INET,SOCK_DGRAM,0);////客戶端不需要綁定}void startClient(){string msg;//接受用戶輸入//發(fā)送給服務(wù)器struct sockaddr_in peer;peer.sin_family=AF_INET;peer.sin_port=htons(_port);peer.sin_addr.s_addr=inet_addr(_ip.c_str());for(;;){cout<<"【請(qǐng)輸入:】";cin>>msg;if(msg=="quit")break;//如果用戶輸入退出,下線sendto(_sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));//客戶端給服務(wù)端發(fā)送消息//服務(wù)器接受消息會(huì)返回信息char echo[128];ssize_t ret=recvfrom(_sock,echo,sizeof(echo)-1,0,nullptr,nullptr);//不關(guān)心服務(wù)器的地址if(ret > 0){echo[ret]='\0';cout<<"客戶端受到回應(yīng)"<< echo<< endl;}}}~udpClient(){close(_sock);}};udpClient.cpp
#include "udpClient.h"int main(int argc,char* argv[]) {if(argc!=3){cout<<"服務(wù)器地址沒有傳入"<<endl;exit(1);}udpClient uu(argv[1],atoi(argv[2]));uu.initClient();//初始化客戶端uu.startClient();//啟動(dòng)客戶端 }(3)效果
使用netstat nlup可以查看網(wǎng)絡(luò)進(jìn)程信息
1:本地環(huán)回測(cè)試
2:局域網(wǎng)IP
3:公網(wǎng)IP
總結(jié)
以上是生活随笔為你收集整理的2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C语言重点难点精讲】C语言指针
- 下一篇: iOS --高仿QQ空间页面