1. 程式人生 > >網路程式設計(一)——TCP程式設計基礎

網路程式設計(一)——TCP程式設計基礎

目錄

1.基礎知識

1.1 IP協議

1.1.1  IP地址的分類

1.1.2 子網掩碼

1.1.3 網路位元組序

1.2傳輸控制協議(TCP)

1.2.1 TCP傳輸的特點

1.2.2 TCP的資料格式

1.2.3 建立連線與斷開連線

1.3.4 TCP的封裝與解封過程

2.基本資料結構與介面

2.1 sockaddr和sockaddr_in

2.2 使用者層與核心層的互動過程

2.2.1 向核心空間傳入資料的互動過程

2.3 TCP網路程式設計流程

2.4 幾個重要的函式詳解

2.4.1 socket()

2.4.2 bind()

2.4.3 listen()

2.4.4 accept()

2.4.5 connect()

3. 一個簡單的伺服器/客戶端的例子


1.基礎知識

1.1 IP協議

版本(4位) 首部長度(4位) 服務型別(8位) 總長度(16位)
標識(16位) 標識(3位) 片偏移(13位)
生存時間TTL(8位) 協議型別(8位) 頭部校驗和(16位)
源IP地址(32位)
目的IP地址(32位)
選項(32位)
資料
  • 首部長度——整個黃色區域的長度,以32位的字為單位。
  • 服務型別如下>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
欄位 優先權 D T R F 保留
長度(位) 3 1 1 1 1 1
含義 優先順序 延遲 吞吐量 可靠性 費用 未用

服務型別欄位由應用程式進行設定,路由器僅在必要的時候進行讀取,不進行設定。

  • 總長度——包括頭部跟資料
  • TTL——源主機在傳送報文時設定TTL(一般為32或64),表示資料報文最多可以經過的路由器的數量。它指定報文的生存時間,每經過一個路由器TTL減1,當TTL為0的時候,路由器丟棄此包,併發送一個ICMP報文來通知源主機。
  • 協議型別如下>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
協議型別 值2 協議型別3
1 ICMP 6 TCP
2 IGMP 17 UDP
  • IP選項——用來標識是正常資料還是用來做網路控制的資料
  1. 安全與處理限制
  2. 路徑記錄:記錄所經歷路由器的IP地址
  3. 寬鬆源站路由:指定資料報文必須經歷的IP地址,可以經過沒有指定的IP地址
  4. 嚴格的源站路由:指定資料報文必須經歷的IP地址,不能經過沒有指定的IP地址

1.1.1  IP地址的分類

IP 地址可以分為A、B、C、D、E類

  • A類網路
網路標識佔位
(1B)設為0
網路ID(7Bit)
支援127個網路
主機ID(24Bit)
  • B類網路
網路標識佔位
(2B)設為10
網路ID(14Bit) 主機ID(16Bit)
  • C類網路
網路標識佔位
(3B)設為110
網路ID(21Bit) 主機ID(8Bit)
  • D類網路(常用語組播)
網路標識佔位
(4B)設為1110
網路ID(28Bit)
  • E類地址:保留,前四位為1111.

IP 32位都為0,表示主機本身。

1.1.2 子網掩碼

子網掩碼的主要作用:

  • 便於網路裝置的儘快定址,區分本網段地址和非本網段地址
  • 劃分子網,進一步縮小子網的地址空間

1.1.3 網路位元組序

網路位元組序,預設為大端位元組序。進行網路位元組序轉換的函式有以下幾個

  • htons():對於short型別,從主機位元組序轉為網路位元組序
  • ntohs():對於short型別,從網路位元組序轉為主機位元組序
  • htonl(): 對於long型別,從主機位元組序轉為網路位元組序
  • ntohl(): 對於long型別,從網路位元組序轉為主機位元組序
#if ISLE
long htonl(long value)
{
    return ((value <<24) | ((value <<8)&0x00ff0000) |
	((value >> 8)&0x0000ff00) | (value >> 24));
}
#else if ISBE
long htonl(long value)
{
	return value;
}
#enif

1.2傳輸控制協議(TCP)

1.2.1 TCP傳輸的特點

  • 位元組流服務
  • 面向連線服務
  • 可靠性傳輸
  • 緩衝傳輸
  • 全雙工傳輸
  • 流量控制

1.2.2 TCP的資料格式

TCP資料在IP報文中的位置如下:

TCP報文的資料格式如下:

源埠號(16位) 目的埠號(16位)
序列號(32位)
確認號(32位)
頭部長度(4位) 保留
(6位)
URG ACK PSH RST SYN FIN 視窗尺寸(16位)
TCP校驗和(16位) 緊急指標(16位)
選項(32位)
資料
  • 序列號——表示分配給TCP的編號。序列號用來標識應用程式從TCP的傳送端到接收端傳送的位元組流。TCP開始連線——>傳送一個序列號(seq)給接收端——>連線成功——>初始序列號(INS)=seq——>連線成功後第1、2...N個位元組是INS+1、INS+2...INS+n(序列號是unsigned int,當達到最大值後從0開始).
  • 確認號——傳送方對傳送的首位元組進行了編號,當接收方接收成功之後,傳送回接收成功的序列號+1表示確認,傳送方再發送的時候從確認號開始.
  • 控制位
列1 列2
欄位 含義
URG 緊急指標欄位
ACK 表示確認號有效
PSH 表示接收方應該迅速將此資料交給應用層
RST 重建連線
SYN 用於發起一個TCP連線
FIN 表示將要斷開TCP連線
  • 視窗尺寸——表示本機上TCP協議可以接收的以位元組為單位的資料
  • 校驗和——校驗TCP頭部跟資料
  • 選項——TCP連線通常在第一個報文中指明這個選項,它指明當前主機所能接收的最大報文長度。

1.2.3 建立連線與斷開連線

建立連線過程如下:

斷開連線過程如下:

1.3.4 TCP的封裝與解封過程

2.基本資料結構與介面

2.1 sockaddr和sockaddr_in

結構struct sockaddr和struct sockaddr_in的大小是完全一致的,所以進行地址結構設定時,通常的方法是利用struct sockaddr_in進行設定,然後強制轉換為struct sockaddr型別。

2.2 使用者層與核心層的互動過程

2.2.1 向核心空間傳入資料的互動過程

向核心傳入資料的函式有send()、bind()等,從核心得到資料的函式有acept()、recv()等。傳入的過程入下圖所示,bind()函式向核心傳入的引數有套接字地址結構和結構的長度兩個引數。引數addlen表示地址結構的長度,my_addr表示指向sockaddr的指標。呼叫bind(),地址結構通過記憶體複製的方式將其中的內容複製到核心,地址結構的長度通過傳值的方式傳入核心,核心按照使用者傳入的地址結構長度和地址結構的首地址來複制套接字的內容。

2.2.2 核心傳出資料的互動過程

傳出過程與傳入過程不同的是,表示地址結構長度的引數在傳入過程中是傳值,而在傳出過程中是通過傳地址完成的。核心按照使用者傳入的地址結構長度進行套接字地址結構資料的複製,將核心中的地址結構資料複製到使用者傳入的地址結構指標中。

2.3 TCP網路程式設計流程

2.4 幾個重要的函式詳解

2.4.1 socket()

socket函式主要用於建立一個協議族為domin,協議型別為type、協議編號為protocol的套接字檔案描述符,shibai

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domin, int type, int protocol)

應用層socket與核心函式之間的關係:

使用者呼叫函式sock = socket(AF_INET,SOCK_STREAM,0)後會執行系統呼叫sys_socket(AF_INET, SOCK_STREAM, 0) ,而sys_socket分為兩部分,一部分生成核心socket結構,另一部分將socket結構與檔案描述符繫結並將繫結後的fd傳遞給應用層。

2.4.2 bind()

bind()函式將長度為addrlen的struct sockadd型別的引數my_addr與sockfd繫結在一起,將sockfd繫結到某個埠上,如果使用connect()函式則沒有繫結的必要。返回0表示成功,-1表示失敗。

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen)

應用層bind()與核心函式之間的關係如下:

2.4.3 listen()

#include <sys/socket.h>
int listen(int sockfd, int backlog)
  • backlog:表示在accept()之前在等待佇列中的客戶端的長度,如果超過這個長度,客戶端會返回一個ECONNREFUSED錯誤。

linsten()僅對SOCK_STREAM或SOCK_SEQPACKET有效,對其它型別將返回錯誤。執行成功返回0,失敗-1.

應用層listen與核心之間的關係如下:

2.4.4 accept()

accept可以得到成功連線的客戶端的IP地址、埠和協議等資訊,這個資訊是通過引數addr獲取的,當accept返回的時候,會將客戶端的資訊儲存在引數addr中。accept函式的返回值是新連線客戶端套接字檔案描述符,與客戶端之間的溝通是通過這個描述符來操作的,而不是通過建立套接字時的檔案描述符。

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

應用層accept與核心之間的關係如下:

2.4.5 connect()

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *server_addr, int addrlen);

應用層connect與核心之間的關係如下:

 

3. 一個簡單的伺服器/客戶端的例子

<<<<<server.c>>>>>>>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 9999
#define BACKLOG 2

void process_conn_server(int s)
{
	ssize_t size = 0;
	char buffer[1024] = {0};
	for(;;){
		size = read(s, buffer, 1024);
		if(size == 0){
			return;
		}
		/*構建響應字元,為接收客戶端位元組的數量*/
		sprintf(buffer,"%d bytes altogether\n",size);
		write(s, buffer, strlen(buffer)+1);
	}
}

int main(int argc,char **argv)
{
	int ss,sc;
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	int err;
	pid_t pid;

	/*建立一個流式套接字*/
	ss = socket(AF_INET, SOCK_STREAM, 0);
	if(ss < 0){
		printf("socket error\n");
		return -1;
	}

	/*設定服務端地址*/
	bzero(&server_addr, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	/*本地地址*/
	server_addr.sin_port = htons(PORT);

	/*繫結套接字到套接字描述符*/
	err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
	if(err < 0){
		printf("bind error\n");
		return -1;
	}

	/*設定偵聽*/
	err = listen(ss, BACKLOG);
	if(err < 0){
		printf("listen error\n");
		return -1;
	}

	for(;;){
		socklen_t addrlen = sizeof(struct sockaddr);
		sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);
		if(sc < 0){
			continue;
		}

		/*建立一個新的程序處理連線*/
		pid = fork();
		if(pid ==0){
			close(ss);
			process_conn_server(sc);
		}else{
			close(sc);
		}
	}
	
}

<<<<<client.c>>>>>>>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 9999

void process_conn_client(int s)
{
	ssize_t size = 0;
	char buffer[1024] = {0};
	
	for(;;){
		size = read(0, buffer, 1024);/*從標準輸入讀取資料到buffer*/
		if(size > 0){
			write(s,buffer,size);
			size = read(s,buffer,1024);
			write(1,buffer,size);
		}
	}
}

int main(int argc,char **argv)
{
	int s;
	struct sockaddr_in server_addr;
	
	s = socket(AF_INET, SOCK_STREAM, 0);
	if(s < 0){
		printf("socket error\n");
		return -1;
	}

	bzero(&server_addr, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	/*本地地址*/
	server_addr.sin_port = htons(PORT);

	/*將輸入的字串轉為IP地址型別*/
	inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
	connect(s,(struct sockaddr*)&server_addr,sizeof(struct sockaddr));
	process_conn_client(s);
	
	close(s);
	return 0;
}

測試結果: