本文主要記錄TCP/UDP網絡編程的基礎知識,采用TCP/UDP實現宿主機和目標機之間的網絡通信。
內容目錄
-
目標
2.Linux網絡編程基礎
2.1 嵌套字2.2 端口
2.3 網絡地址
2.3.1 網絡地址的格式
2.3.2 網絡地址的轉換
2.4 字節序
3.TCP
3.1 TCP流程圖
3.2 TCP步驟分析
3.3 TCP完整代碼
3.4 測試結果
4.UDP
4.1 UDP流程圖
4.2 UDP步驟分析
4.3 UDP完整代碼
4.4 測試結果
1. 目標
實現讓兩個設備通過網絡傳輸數據,比如開發板和Linux主機之間傳數據,
以后就可以實現開發板通過網絡上報數據或者 主機通過網絡控制開發板 。
此外,暫時不想關心具體的網絡模型,更注重于網絡相關函數的直接使用。
2.Linux網絡編程基礎
2.1 嵌套字
多個TCP連接或者多個應用程序進程 可能需要同一個TCP端口傳輸數據。
為了區分不同應用程序進程和連接,許多計算機操作系統為應用程序與TCP/IP交互提供了稱為 嵌套字(Socket) 的接口。
Linux中的網絡編程正是通過Socket接口實現的,Socket是一種文件描述符。
常用的TCP/IP有以下三種類型的嵌套字:
-
流式嵌套字(SOCK_STREAM)
用于提供面向連接的、可靠的數據傳輸服務,即使用TCP進行傳輸。
-
數據報嵌套字(SOCK_DGRAM)
用于提供無連接的服務,即使用UDP進行傳輸。
-
原始嵌套字(SOCK_RAW)
可以讀寫內核沒有處理的IP數據報,而流式嵌套字只能讀取TCP的數據,數據報嵌套字只能讀取UDP的數據。
因此,如果要訪問其它協議發送的數據必須使用原始嵌套字,它允許對底層協議(如IP或ICMP)直接訪問。
2.2 端口
TCP/IP協議中的端口,端口號的范圍從0~65535。
一類是由互聯網指派名字和號碼公司ICANN負責分配給一些常用的應用程序固定使用的“周知的端口”,其值一般為0~1023。例如http的端口號是80,FTP為21,SSH為22,Telnet為23等。
還有一類是用戶自己定義的,通常是大于1024的整型值。
2.3 網絡地址
網絡通信,歸根到底還是進程間的通信(不同計算機上的進程間通信)。
在網絡中,每一個節點(計算機或路由)都有一個網絡地址,如192.168.1.4,也就是IP地址。
兩個進程通信時,首先要確定各自所在的網絡節點的網絡地址。
但是,網絡地址只能確定進程所在的計算機,而一臺計算機上很可能同時運行著多個進程,所以僅憑網絡地址還不能確定到底是和網絡中的哪一個進程進行通信,因此套接口中還需要包括其他的信息,也就是端口號(PORT)。
在一臺計算機中,一個端口號一次只能分配給一個進程,也就是說,在一臺計算機中,端口號和進程之間是一一對應關系。
所以, 使用端口號和網絡地址的組合可以唯一的確定整個網絡中的一個網絡進程 。
例如,如網絡中某一臺計算機的IP為192.168.1.4,操作系統分配給計算機中某一應用程序進程的端口號為1500,則此時192.168.1.4 1500
就構成了一個套接口。
2.3.1 網絡地址的格式
在Socket程序設計中,struct sockaddr
用于記錄網絡地址,其格式如下:
1struct sockaddr
2{
3 unsigned short sa_family; /*協議族,采用AF_XXX的形式,例如AF_INET(IPv4協議族)*/
4 char sa_data[14]; /*14字節的協議地址,包含該socket的IP地址和端口號。*/
5};
但在實際編程中,并不針對sockaddr
數據結構進行操作,而是用與其等價的sockaddr_in
數據結構:
1struct sockaddr_in
2{
3 short int sa_family; /*地址族*/
4 unsigned short int sin_port; /*端口號*/
5 struct in_addr sin_addr; /*IP地址*/
6 unsigned char sin_zero[8]; /*填充0 以保持與struct sockaddr同樣大小*/
7};
2.3.2 網絡地址的轉換
IP地址通常用數字加點(如192.168.1.4)表示,而在struct in_addr
中使用的式32位整數表示。因此,Linux提供如下函數進行兩者之間的轉換:
- inet_aton()函數:
所需要頭文件 :
#include
#include
#include
函數格式 :
int inet_aton(const char *cp, struct in_addr *inp);
函數功能 :
將a.b.c.d字符串形式的IP地址轉換成32位網絡序號IP地址;
*cp:存放字符串形式的IP地址的指針
*inp:存放32位的網絡序號IP地址
返回值 :
轉換成功,返回非0,否則返回0;
- inet_ntoa()函數:客戶機端:
所需要頭文件 :
#include
#include
#include
函數格式 :
char *inet_ntoa(struct in_addr in);
函數功能 :
將32位網絡序號IP地址轉換成a.b.c.d字符串形式的IP地址;
in:Internet主機地址的結構
返回值 :
轉換成功,返回一個字符指針,否則返回NULL;
2.4 字節序
不同的CPU采用對變量的字節存儲順序可能不同。
常用的X86結構是小端模式,很多的ARM,DSP都為小端模式,即內存的低地址存儲數據的低字節,高地址存儲數據的高字節。
而KEIL C51則為大端模式,內存的高地址存儲數據的低字節,低地址存儲數據高字節。
對于網絡傳輸來說,數據順序必須是一致的,網絡字節順序采用大端字節序方式。
下面是四個常用的轉換函數:
主機轉網絡:
- htons()函數:
所需要頭文件 :
#include
函數格式 :
unsigned short int htons(unsigned short int hostshort)
函數功能 :
將參數指定的16位主機(host)字符順序轉換成網絡(net)字符順序;
hostshort:待轉換的16位主機字符順序數
返回值 :
返回對應的網絡字符順序數;
- htonl()函數:
所需要頭文件 :
#include
函數格式 :
unsigned long int htons(unsigned long int hostlong)
函數功能 :
將參數指定的32位主機(host)字符順序轉換成網絡(net)字符順序;
hostlong:待轉換的32位主機字符順序數
返回值 :
返回對應的網絡字符順序數;
網絡轉主機:
- ntohs()函數:
所需要頭文件 :
#include
函數格式 :
unsigned short int ntohs(unsigned short int netshort)
函數功能 :
將參數指定的16位網絡(net)字符順序轉換成主機(host)字符順序;
netshort:待轉換的16位網絡字符順序數
返回值 :
返回對應的主機字符順序數;
- ntohl()函數:
所需要頭文件 :
#include
函數格式 :
unsigned long int ntohl(unsigned long int netlong)
函數功能 :
將參數指定的32位網絡(net)字符順序轉換成主機(host)字符順序;
netshort:待轉換的32位網絡字符順序數
返回值 :
返回對應的主機字符順序數;
3.TCP
TCP有專門的傳遞保證機制,收到數據時會自動發送確認消息,發送方收到確認消息后才會繼續發送消息,否則繼續等待。
這樣的好處是傳輸的數據是可靠的,此外它是有連接的傳輸,大多數網絡傳輸都是用的TCP。
3.1 TCP流程圖
3.2 TCP步驟分析
程序分為服務器端和客戶機端,先從服務器端開始分析。
-
服務器端:
a. 創建socket
1 sock_fd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET:IPV4;SOCK_STREAM:TCP
2 if (-1 == sock_fd)
3 {
4 fprintf(stderr,"socket error:%s\\n\\a", strerror(errno));
5 exit(1);
6 }
所需要頭文件 :
#include
#include
函數格式 :
int socket(int domain, int type, int protocol);
函數功能 :
創建一個套接字;
domain:協議域(族), 決定了套接字的地址類型 ,例如AF_INET決定了要用IPv4地址(32位)與端口號(16位)的組合。常見的協議族有: AF_INET 、AF_INET6、AF_LOCAL(或稱AF_UNIX)、AF_ROUTE等;
type: 指定套接字類型 , SOCK_STREAM (TCP)、 SOCK_DGRAM (UDP)、SOCK_RAW
protocol:指定socket所使用的傳輸協議編號,通常為0
返回值 :
若成功,返回一個套接字描述符,否則返回-1;
Socket就是一種文件描述符,和普通的打開文件一樣,需要檢測其返回結果。
b. 設置socket
1 memset(&server_addr, 0, sizeof(struct sockaddr_in));//clear
2 server_addr.sin_family = AF_INET;
3 server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY:This machine all IP
4 server_addr.sin_port = htons(PORT_NUMBER);
設置何種協議族,設置本機IP和端口,也就有了唯一性。
c. 綁定socket
1 ret = bind(sock_fd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr));
2 if(-1 == ret)
3 {
4 fprintf(stderr,"bind error:%s\\n\\a", strerror(errno));
5 close(sock_fd);
6 exit(1);
7 }
所需要頭文件 :
#include
#include
函數格式 :
int bind(int sockfd, struct sockaddr *addr, int addrlen);
函數功能 :
把套接字綁定到本地計算機的某一個端口上;
sockfd:待綁定的套接字描述符
addr:一個struct sockaddr *指針,指定要綁定給sockfd的協議地址。內容結構由前面的協議族決定。
addrlen:地址的長度
返回值 :
若成功,返回0,否則返回-1,錯誤信息存在errno中;
d. 開始監聽
1 ret = listen(sock_fd, BACKLOG);
2 if (-1 == ret)
3 {
4 fprintf(stderr,"listen error:%s\\n\\a", strerror(errno));
5 close(sock_fd);
6 exit(1);
7 }
所需要頭文件 :
#include
#include
函數格式 :
int listen(int sockfd, int backlog);
函數功能 :
使服務器的這個端口和IP處于監聽狀態,等待網絡中某一客戶機的連接請求,最大連接數量為backlog≤128;
sockfd:待監聽的套接字描述符
backlog:最大可監聽和連接的客戶端數量
返回值 :
若成功,返回0,否則返回-1;
e. 阻塞,等待連接
1 addr_len = sizeof(struct sockaddr);
2 new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_len);
3 if (-1 == new_fd)
4 {
5 fprintf(stderr,"accept error:%s\\n\\a", strerror(errno));
6 close(sock_fd);
7 exit(1);
8 }
1
所需要頭文件 :
#include
#include
函數格式 :
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
函數功能 :
接受連接請求,建立起與客戶機之間的通信連接。服務器處于監聽狀態時,如果某時刻獲得客戶機的連接請求,此時并不是立即處理這個請求,而是將這個請求放在等待隊列中,當系統空閑時再處理客戶機的連接請求;
當accept函數接受一個連接時,會返回一個新的socket標識符,以后的數據傳輸和讀取就要通過這個新的socket編號來處理,原來參數中的socket也可以繼續使用,繼續監聽其它客戶機的連接請求;
accept連接成功時,參數addr所指的結構體會填入所連接機器的地址數據;
sockfd:待監聽的套接字描述符
addr:指向struct sockaddr的指針,用于返回客戶端的協議地址
addrlen:協議地址的長度
返回值 :
若成功,返回一個由內核自動生成的一個全新描述字,代表與返回客戶的TCP連接,否則返回-1,錯誤信息存在errno中;
f. 接收數據
1 recv_len = recv(new_fd, recv_buf, 999, 0);
2 if (recv_len <= 0)
3 {
4 fprintf(stderr, "recv error:%s\\n\\a", strerror(errno));
5 close(new_fd);
6 exit(1);
7 }
8 else
9 {
10 recv_buf[recv_len] = '\\0';
11 printf("Get msg from client%d: %s\\n", client_num, recv_buf);
12 }
所需要頭文件 :
#include
#include
函數格式 :
int recv(int sockfd, void *buf, size_t len, int flags);
函數功能 :
用新的套接字來接收遠端主機傳來的數據,并把數據存到由參數buf指向的內存空間;
sockfd:sockfd為前面accept的返回值,即new_fd,也就是新的套接字
buf:指明一個緩沖區
len:指明緩沖區的長度
flags:通常為0
返回值 :
若成功,返回接收到的字節數,另一端已關閉則返回0,否則返回-1,錯誤信息存在errno中;
g. 關閉socket
1 close(sock_fd);
2 exit(0);
為了應對多個連接,并保證它們之間相互獨立,實際編程中往往還要加入多進程fork()。
讓子進程接收數據,父進程繼續監聽新的連接。
- 客戶機端:
a. 創建socket
1 sock_fd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET:IPV4;SOCK_STREAM:TCP
2 if (-1 == sock_fd)
3 {
4 fprintf(stderr,"socket error:%s\\n\\a", strerror(errno));
5 exit(1);
6 }
b. 設置socket
1 memset(&server_addr, 0, sizeof(struct sockaddr_in));//clear
2 server_addr.sin_family = AF_INET;
3 server_addr.sin_port = htons(PORT_NUMBER);
其中注意的是,這里設置的socket內容是指 希望連接的服務器IP和端口號信息,IP地址來自用戶的輸入,并轉換格式得到。因此,這里的設置和服務器的設置,要保持內容上的一致。
1 ret = inet_aton(argv[1], &server_addr.sin_addr);
2 if(0 == ret)
3 {
4 fprintf(stderr,"server_ip error.\\n");
5 close(sock_fd);
6 exit(1);
7 }
c. 連接
1 ret = connect(sock_fd, (const struct sockaddr *)&server_addr, sizeof(struct sockaddr));
2 if (-1 == ret)
3 {
4 fprintf(stderr,"connect error:%s\\n\\a", strerror(errno));
5 close(sock_fd);
6 exit(1);
7 }
-
主機
+關注
關注
0文章
985瀏覽量
35062 -
TCP
+關注
關注
8文章
1349瀏覽量
78985 -
UDP
+關注
關注
0文章
322瀏覽量
33876 -
網絡通信
+關注
關注
4文章
792瀏覽量
29759 -
網絡編程
+關注
關注
0文章
71瀏覽量
10067
發布評論請先 登錄
相關推薦
評論