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