1. 程式人生 > >網路程式設計套接字、網路位元組序及用udp寫客戶端和服務端聊天程式

網路程式設計套接字、網路位元組序及用udp寫客戶端和服務端聊天程式

認識IP地址 IP協議有兩個版本:IPV4和IPV6。 IPV4:IPV4版本的IP地址是4位元組無符號整數。那麼就存在IP地址資源匱乏的時候,這時可以採用兩種方法: DHCP:ip地址動態分配(應用層協議); NAT: 地址替換; 但是這兩種方法只是暫時的有IP地址,但並不能從本質上解決IP資源不夠的問題。 IPV6:IPV6版本的IP地址是128位元組無符號整數。這個可以從本質上解決IP資源匱乏問題,但是由於不向下相容IPV4,所以一各大廠商不採用IPV6版本協議。 注:後文凡是提到IP協議,沒有特殊說明,都預設為是IPV4協議。

  • IP地址在IP協議中,用來標識網路中不同主機的地址,唯一標識不同的主機;
  • 通常使用“點分十進位制”的字串表示IP地址,如192.168.0.1;用點分割的每一個數字表示一個位元組,每個位元組範圍是0-255;255.255.255.255是廣播地址,0.0.0.0是無效地址,這兩個地址都使用不了; 源IP地址和目的IP地址 在IP資料包頭部中,有兩個IP地址,分別為源IP地址和目的IP地址。但是隻憑藉IP地址就可以完成通訊嗎?比如說用微信發訊息,有了IP地址就能夠把訊息發到對方的機器的微信上嗎?還需要有一個其他的標識來區分出這個資料要給哪個程式進行解析。這個標識就是埠號。 埠號 埠號是傳輸層協議的內容:
  • 埠號是一個2位元組16位的無符號整數;(0-65535之間一個數字,0-1024不推薦使用)
  • 埠號用來標識一個程序,告訴作業系統,當前資料要交給哪一個程序來處理。(為什麼不用pid=getpid()來標識一個程序,因為pid會變,如一個程序關閉再開啟後,pid就會變,而一個程序的埠號不會變)
  • IP地址+埠號能夠唯一標識網路上某一主機的某一程序; 源埠號和目的埠號 傳輸層協議(TCP和UDP)的資料段有兩個埠號,分別為源埠號(資料是誰發)和目的埠號(資料發給誰)。 傳輸層兩個協議(TCP和UDP) 傳輸層有2個協議:tcp(傳輸控制協議)和udp(使用者資料報協議 )協議,兩個協議各有不同的特點和應用場景,協議如何進行資料傳輸,取決於協議的應用場景和當前使用場景,那麼兩個協議的特點就十分重要。 TCP協議:
    資料可靠傳輸,可連線,面向位元組流傳輸。 UDP協議: 資料不可靠傳輸,不可連線,面向資料報傳輸。 **可靠傳輸:**資料能夠安全全部發送;(TCP不會丟棄資料,而UDP可能會丟棄資料) 可連線: TCP連線就像打電話,你撥特定的號碼,對方線上並接起電話,然後通話,通完之後再結束通話,整個過程是相互聯絡的,缺一不可; UDP不可連線就像寄信,你寄過去資訊,對方有沒有收到不知道,而且對方回不回答也不知道,對方對你送訊息也是一樣。 面向位元組流: 優點:收發資料比較靈活,可以分多次傳送,多次接受; 缺點:資料沒有明顯邊界(分多次傳送,造成資料沒有明顯邊界),容易造成tcp粘包問題; 面向資料報: 缺點:資料傳送有最大長度限制,如果傳送資料超過最大長度限制,將會丟棄該資料,接受的時候一條一條接受,不能分割,如果接受的資料超過最大長度,將傳送失敗 優點:由於資料有邊界,不會造成tcp粘包問題; 網路位元組序----大小端 cpu在對資料在記憶體中進行存取時的方向不同分出了大小端。 0x04 03 02 01 (低位是01,高位是04) 大端:低地址處存低位; 小端:低地址處存高位; MIPS—RISC處理器 ----大端 x86處理器 ----小端 如何判斷大小端:
//聯合體判斷大小端
#include<stdio.h>
union U
{
        int a;
        unsigned char c;
}u;
int main()
{
        u.a=1;
        if(u.c==1)
        {   
                printf("intel\n");
        }   
        else
        {   
                printf("motel\n");
        }   
        return 0;
}
//直接判斷大小端
#include<stdio.h>
int main()
{
        int a=1;
        //定義一個int變數,強轉後[0]是低地址,低地址是1是小端,低地址是0是大端
        if(((unsigned char*)(&a))[0]==1)
        {   
                printf("intel\n"); //小端
        }   
        else
        {   
                printf("motor\n"); //大端
        }   
        return 0;
}

記憶體中的多位元組資料相對記憶體地址有大端和小端之分,磁碟檔案中的多位元組資料相對與檔案中的偏移地址也有大端和小端之分,網路資料流也有大端小端之分, 那麼如何定義⺴⽹網路資料流的地址呢? 1.傳送主機通常將傳送緩衝區中的資料按記憶體地址從低到高的順序發出; 2.接收主機把從網路上接到的位元組依次儲存在接收緩衝區中,也是按記憶體地址從低到高的順序儲存; 3.因此,網路資料流的地址應這樣規定:先發出的資料是低地址,後發出的資料是高地址. 4.TCP/IP協議規定,網路資料流應採用大端位元組序,即低地址高位元組. 5.不管這臺主機是大端機還是小端機, 都會按照這個TCP/IP規定的網路位元組序來發送/接收資料; 6.如果當前傳送主機是小端, 就需要先將資料轉成大端; 否則就忽略, 直接傳送即可; 這裡寫圖片描述 socket套接字程式設計 socket是一套介面,用於網路程式設計的介面,同時socket也是一個數據結構; 想要開始網路程式設計,就需要先建立一個套接字,也就是對網路程式設計來說,第一步永遠是建立套接字,套接字建立成功後,才可以通過對套接字的操作,來完成網路上資料的傳輸。 網路通訊是網路上兩個主機上的程序間的通訊,這兩個主機有一個區分:一個主機為客戶端,一個主機是服務端,並且永遠是客戶端向服務端發起請求。即服務端在明,客戶端在暗。如qq:客戶端十分多,騰訊的伺服器不知道客戶端是誰,在哪裡,所以無法向客戶端推送訊息,但是騰訊伺服器地址是固定的,埠是固定的,只要有人下載了客戶端,這些服務端資訊都已經封裝在qq軟體中。 udp網路程式設計步驟: 1.建立套接字(功能:建立與網絡卡的聯絡,協議版本的選擇,傳輸層協議的選擇) 2.為套接字繫結地址資訊(ip地址,埠(port)) 3.接受資料 4.傳送資料 5.關閉socket描述符 1.建立套接字

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址域
		 AF_INET :ipv4協議
type: 套接字型別
		SOCK_STREAM 流式套接字
		SOCK_DGRAM  資料報套接字
protocol :協議型別 
		如果是0,則表示預設;流式套接字預設tcp協議,報式套接字預設udp協議
		流式套接字: IPPROTO_TCP 6 
		報式套接字:IPPROTO_UDP 17  
如:socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
返回值:成功:套接字描述符 
	   失敗:-1						

2.為socket繫結地址資訊確定socket中資訊(埠號和IP地址),即確定誰能夠操作緩衝區。

 #include <sys/socket.h>
 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 引數: sockfd: socket描述符
 	   addr :socket繫結的地址
 	   addrlen :地址資訊長度
 返回值:成功:0(網絡卡操作那個程序),失敗 -1
 sockaddr結構:
 struct sockaddr {
               sa_f  amily_t sa_family;
               char        sa_data[14];
                }
雖然bind裡引數是sockaddr,但是真正在基於IPV4程式設計時,使用的結構體是sockaddr_in;這個結構體裡主要有三部分資訊:地址型別,埠號,IP地址。
sockaddr_in在標頭檔案#include<netinet/in.h>或#include<arpa/inet.h>中定義。該結構體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變數中,如下: 
structsockaddr_in{

short           sin_family;//AF_INET(地址族)PF_INET(協議族)

unsigned short  sin_port;/*Portnumber(必須要採用網路資料格式,普通數字可以用htons()函式轉換成網路資料格式的數字)*/

struct in_addr  sin_addr;//32位IP地址

unsigned char   sin_zero[8];//沒有實際意義,只是為了跟SOCKADDR結構在記憶體中對齊*/

};
該結構體中提到的另一個結構體in_addr定義如下,它用來存放32位IP地址:
typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};   
in_addr用來表示一個IPV4的IP地址,其實是一個32位整數。   

客戶端不推薦手動繫結地址資訊 ,因為繫結有可能因為特殊原因失敗,但是客戶端具體使用哪個地址和埠都可以,只要能把資料傳送出去,所以客戶端程式不手動繫結地址,直至傳送資料時,作業系統檢測到socket沒有繫結地址,會自動選擇合適的地址和埠為socket繫結地址,這種資料一般不會出錯。 3.接受資料(網絡卡從緩衝區把資料拷到資料態))

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
socket : socket描述符
buf:接受資料儲存在buff 
len :想要接受多少位元組資料
flags :如果是0:如果緩衝區沒有資料,將一直等待,即阻塞等待 
src_addr :用於確定是哪一個對端傳送的資料,即確定對端地址資訊
addrlen : 地址資訊長度 
返回值:成功:實際從緩衝區接受資料長度
       失敗: -1

4.傳送資料

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd : 傳送資料的時候就是通過這個socket所繫結的地址來發送
buf :要傳送的資料
flags : 0 預設阻塞式傳送
dest_addr :資料要傳送到的對端地址
addrlen :dest_addr地址資訊長度  
返回值: 成功:實際傳送資料長度
        失敗 :-1

接下里將會用udp寫一個簡單的客戶端和服務端之間的聊天程式: 首先:客戶端程式碼:

//udp客戶端
//1.建立套接字
//2.繫結地址資訊
//3.傳送資料
//4.接受資料
//5.關閉套接字

#include<stdio.h>
#include<errno.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>

int main()
{
        //1.建立套接字
        int sockfd=socket(AF_INET,SOCK_DGRAM,0);//udp協議
        if(sockfd<0)
        {   
                perror("socket errno");
                return -1; 
        }   
        //2.繫結地址資訊

        while(1){    
                //3.傳送資料 :服務端先發送資料
                //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                //              const struct sockaddr *dest_addr, socklen_t addrlen);
                struct sockaddr_in ser_addr;
                ser_addr.sin_family=AF_INET;
                ser_addr.sin_addr.s_addr=(inet_addr)("192.168.61.128");//inet_addr將字串轉換成網路位元組序
                ser_addr.sin_port=(htons)(9000);//將埠號轉換成2個位元組網路位元組序
                char buff[1024];
                memset(buff,0x00,1024);
                printf("please send:\n");
 scanf("%s",buff);
                socklen_t  len=sizeof(struct sockaddr_in);
                ssize_t ret=sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&ser_addr,len);
                if(ret<0)
                {
                        perror("sendto error");
                        close(sockfd);
                        return -1;
                }
                //接受資料
                // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                //                 struct sockaddr *src_addr, socklen_t *addrlen);
                memset(buff,0x00,1024);
                ret=recvfrom(sockfd,buff,1023,0,(struct sockaddr*)&ser_addr,&len);
                if(ret<0)
                {
                        perror("recvfrom error");
                        close(sockfd);  //在任何可能退出的地方關閉sockfd
                        return -1;
                }
                printf("%d\n",ret);
                //從哪個主機的那個埠接收的資料
                printf("[%s:%d:]say:%s\n",(inet_ntoa)(ser_addr.sin_addr), (ntohs)(ser_addr.sin_port),buff); 
                 //char *inet_ntoa (struct in_addr);   
                 //uint16_t ntohs(uint16_t netshort);
       }
        close(sockfd);  //關閉socket描述符
        return 0;
}

接下來是服務端程式碼:

//udp服務端程式碼
//1.建立套接字
//2.為套接字繫結地址資訊
//3.接受資料
//4.傳送資料
//5.關閉stocket描述符

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
int main()
{
        //1.建立套接字
        // int socket(int domain, int type, int protocol);
        int sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//第三個引數是0也可以
        if(sockfd<0)
        {   
                perror("socket error");
                return -1; 
        }   
        //2.為socket繫結地址資訊(繫結的是自己服務端主機IP和埠)
        // int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in ser_addr;
        ser_addr.sin_family=AF_INET;
        ser_addr.sin_port=(htons)(9000); //將埠轉換成網路位元組序
        ser_addr.sin_addr.s_addr=(inet_addr)("192.168.61.128");  //將點分十進位制轉換成網路位元組序
        int len=sizeof(struct sockaddr_in);    
        int ret=bind(sockfd,(struct sockaddr*)&ser_addr,len);
        if(ret<0)
        {   
                perror("bind error");
                close(sockfd);
                return -1; 
 }

        while(1){
                //3.接收資料
                // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flag, struct sockaddr *src_addr, socklen_t *addrlen);
                char buff[1024]={0};
                struct sockaddr_in cli_addr;   //定義對端地址資訊結構體
                len=sizeof(struct sockaddr_in);
                ret=recvfrom(sockfd,buff,1023,0,(struct sockaddr*)&cli_addr,&len);
                if(ret<0)
                {
                        perror("recvfrom error");
                        close(sockfd);
                        return -1;
                }
                //從哪個主機的那個埠接收的資料,這時cli_addr中對端地址資訊
                //inet_ntoa:將網路位元組序轉換成字串;ntohs:將網路位元組序轉換成2位元組埠
                printf("[%s:%d]say:%s\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),buff);
                //4.傳送資料
                //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
                memset(buff,0x00,1024);
                scanf("%s",buff);
                ret=sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&cli_addr,len);
                if(ret<0)
                {
                        perror("sendto error");
                        close(sockfd);
                        return -1;
                }
        }
        close(sockfd);  //關閉socket描述符
        return 0;
}

服務端: 在這裡插入圖片描述 客戶端: 在這裡插入圖片描述