Linux應用編程和網絡編程(11)-------TCP/IP協議的簡單學習與應用socket接口編程

1.網絡的分層結構

因爲網絡是一種非常複雜的通信方式,所以要通過分層來進行開發難度的降低。因此我們在研究網絡通信時,一定要在同一個層次進行研究,不能跨層次研究,比如分析客戶端和服務器的收發時,要分析API層次時,兩部分都要統一在這個層次進行分析,而不能是一端分析API接口,另一端卻去分析驅動了。一般情況下,我們在網絡編程時最關注的是應用層,傳輸層只需要瞭解即可。

2.BS和CS
(1)CS架構介紹(client server,客戶端服務器架構)。比如QQ,360網盤之類的(在電腦上或手機上用軟件登錄的)。
(2)BS架構介紹(broswer server,瀏覽器服務器架構)。比如在線版的QQ,網頁版360網盤(用瀏覽器打開的)。

一,TCP協議的簡單學習

TCP協議
傳輸控制協議(TCP,Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議

1、關於TCP理解的重點

  1. TCP協議工作在OSI七層模型的傳輸層對上服務socket接口對下調用IP層.
  2. TCP協議面向連接,通信前必須先3次握手建立連接關係後才能開始通信。
  3. TCP協議提供可靠傳輸,不怕丟包、亂序等。

2、TCP如何保證可靠傳輸

  1. TCP在傳輸有效信息前要求通信雙方必須先握手,建立連接才能通信
  2. TCP的接收方收到數據包後會ack給發送方,若發送方未收到ack會丟包重傳
  3. TCP的有效數據內容會附帶校驗,以防止內容在傳遞過程中損壞
  4. TCP會根據網絡帶寬來自動調節適配速率(滑動窗口技術)
  5. 發送方會給各分割報文編號,接收方會校驗編號,一旦順序錯誤即會重傳。

建立TCP需要三次握手才能建立,而斷開連接則需要四次握手。整個過程如下圖所示:

在這裏插入圖片描述

3、TCP的三次握手

  1. 首先Client端發送連接請求報文Server段接受連接後回覆ACK報文,併爲這次連接分配資源。Client端接收到ACK報文後也向Server段發生ACK報文,並分配資源,這樣TCP連接就建立了。
  2. 建立連接的條件:服務器listen時客戶端主動發起connect

4、TCP的四次揮手

  1. 斷開連接的條件:服務器或者客戶端都可以主動發起關閉
  2. 斷開過程:假設客戶端先向其TCP發出連接釋放報文段,並停止再發送數據,主動關閉TCP連接。客戶端發送釋放報文FIN [第一次] 。服務器收到釋放報文後發出確認報文ACK [第二次] 。服務器發出的連接釋放報文FIN,並且還附帶上次已發送過的確認號ACK [第三次] 。客戶端在收到服務器的連接釋放報文段後,發送確認報文ACK [第四次] 。

注:這些握手協議已經封裝在TCP協議內部,socket編程接口平時不用管

5、基於TCP通信的服務模式

  1. 具有公網IP地址的服務器(或者使用動態IP地址映射技術)
  2. 服務器端socket、bind、listen後處於監聽狀態
  3. 客戶端socket後,直接connect去發起連接
  4. 服務器收到並同意客戶端接入後會建立TCP連接,然後雙方開始收發數據,收發時是雙向的,而且雙方均可發起
  5. 雙方均可發起關閉連接
  6. 常見的使用了TCP協議的網絡應用:http(相當於一個應用程序,用來傳輸文本信息)、ftp、QQ服務器和mail服務器。這些需要很高可靠的應用,底層都是基於TCP協議的。

二,socket編程接口介紹

1、建立連接

  1. socket:socket函數類似於open,用來打開一個網絡連接,如果成功則返回一個網絡文件描述符(int類型),之後我們操作這個網絡連接都通過這個網絡文件描述符。
  2. == bind==:用來進行綁定的函數,把本地的IP地址和socket進行綁定。功能類似於fctrl函數,是用來改變屬性的函數
  3. listen:監聽一個端口,監聽的是在bind時綁定的那個地址
  4. accept返回值是一個fd,accept正確返回就表示我們已經和前來連接我的客戶端之間建立了一個TCP連接了,以後我們就要通過這個連接來和客戶端進行讀寫操作,讀寫操作就需要一個fd,這個fd就由accept來返回了。(阻塞的位置)
  5. connect:用來連接服務器的(客戶端那邊用)。

2、發送和接收

  1. send/write發送數據。(send比write就多了一個flag,只有支持一些特殊協議時會用到flag,普通情況寫0即可)
  2. recv/read:接收數據。(在網絡中發送有點像是寫文件,在網絡中接收有點像是收文件)

3、輔助性函數

  1. ==inet_aton、inet_addr、inet_ntoa ==:(點分十進制和32位二進制形式互相轉化,不支持IPv6)
  2. inet_ntop、inet_pton:(點分十進制和32位二進制形式互相轉化,支持IPv6)

4、表示IP地址相關數據結構

  1. 都定義在 netinet/in.h

  2. struct sockaddr,這個結構體是網絡編程接口中用來表示一個IP地址的,注意這個IP地址是兼容IPv4和IPv6

  3. typedef uint32_t in_addr_t; 網絡內部用來表示IP地址的類型

  4. struct in_addrstruct in_addr { in_addr_t s_addr; };

  5. struct sockaddr_in

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;                 /* Port number.  */
    struct in_addr sin_addr;            /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
  };
  
  1. struct sockaddr 這個結構體是linux的網絡編程接口中用來表示IP地址的標準結構體,bind、connect等函數中都需要這個結構體,這個結構體是兼容IPV4和IPV6的。在實際編程中這個結構體會被一個struct sockaddr_in或者一個struct sockaddr_in6所填充

三,IP地址格式轉換函數實踐

在進行格式轉換時,這些函數,默認都使用大端模式

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


#define IPADDR	"192.168.1.102"

// 0x66		01	a8		c0
// 102		1	168		192
// 網絡字節序,其實就是大端模式


int main(void)
{
	//使用inet_ntop來轉換
	struct in_addr addr = {0};
	char buf[50] = {0};
	
	addr.s_addr = 0x6601a8c0;
	
	inet_ntop(AF_INET, &addr, buf, sizeof(buf));

	printf("ip addr = %s.\n", buf);   //192.168.1.102
	
	
#if 0
	// 使用inet_pton來轉換
	int ret = 0;
	struct in_addr addr = {0};
	
	ret = inet_pton(AF_INET, IPADDR, &addr);
	if (ret != 1)
	{
		printf("inet_pton error\n");
		return -1;
	}
	
	printf("addr = 0x%x.\n", addr.s_addr);  //0x6601a8c0
	
#endif
	
#if 0    //使用inet_addr來轉換
	in_addr_t addr = 0;
	
	addr = inet_addr(IPADDR);
	
	printf("addr = 0x%x.\n", addr);		// 0x6601a8c0
	
#endif	

	return 0;
}




四,網絡編程實戰


概念端口號,實質就是一個數字編號,用來在我們一臺主機中(主機的操作系統中)唯一的標識一個能上網的進程端口號和IP地址一起會被打包到當前進程發出或者接收到的每一個數據包中。每一個數據包將來在網絡上傳遞的時候,內部都包含了發送方和接收方的信息(就是IP地址和端口號),所以IP地址和端口號這兩個往往是打包在一起不分家的

探討:如何讓服務器和客戶端好好溝通
(1)客戶端和服務器原則上都可以任意的發和收,但是實際上雙方必須配合:client發的時候server就收,而server發的時候client就收
(2)必須瞭解到的一點:client和server之間的通信是異步的,這就是問題的根源
(3)解決方案:依靠應用層協議來解決。說白了就是我們server和client事先做好一系列的通信約定。

自定義應用層協議
1、自定義應用層協議第一步:規定發送和接收方法
(1)規定連接建立後由客戶端主動向服務器發出1個請求數據包,然後服務器收到數據包後回覆客戶端一個迴應數據包,這就是一個通信回合
(2)整個連接的通信就是由N多個回合組成的。

2、自定義應用層協議第二步:定義數據包格式
3、常用應用層協議:http、ftp······

下面以一個例子展示網絡編程:客戶端向服務器註冊學生的基本信息(發送一個數據包),服務器迴應一個數據包表示接收完成(展示學生的基本信息)。

服務器端代碼:server.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERPORT		9003
#define SERADDR		"192.168.1.104"		// ifconfig看到的
#define BACKLOG		100		//貌似是最大能容納的連接數

char recvbuf[100];

#define CMD_REGISTER	1001	// 註冊學生信息
#define CMD_CHECK		1002	// 檢驗學生信息
#define CMD_GETINFO		1003	// 獲取學生信息

#define STAT_OK			30		// 回覆ok
#define STAT_ERR		31		// 回覆出錯了

typedef struct commu
{
	char name[20];		// 學生姓名
	int age;			// 學生年齡
	int cmd;			// 命令碼
	int stat;			// 狀態信息,用來回復
}info;


int main(void)
{
	int sockfd = -1, ret = -1, clifd = -1;
	socklen_t len = 0;
	//這裏的結構爲sockaddr_in結構體包含sin_port和sin_addr結構體,sin_addr結構體包含s_addr
	struct sockaddr_in seraddr = {0};
	struct sockaddr_in cliaddr = {0};
	
	// 第1步:先socket打開文件描述符	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("socket");
		return -1;
	}
	printf("socketfd = %d.\n", sockfd);
	
	// 第2步:bind綁定sockefd和當前電腦的ip地址&端口號
	seraddr.sin_family = AF_INET;		// 設置地址族爲IPv4
	seraddr.sin_port = htons(SERPORT);	// 設置地址的端口號信息
	seraddr.sin_addr.s_addr = inet_addr(SERADDR);	// 設置IP地址
	ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
	if (ret < 0)
	{
		perror("bind");
		return -1;
	}
	printf("bind success.\n");
	
	// 第3步:listen監聽端口
	ret = listen(sockfd, BACKLOG);		// 阻塞等待客戶端來連接服務器
	if (ret < 0)
	{
		perror("listen");
		return -1;
	}
	
	// 第4步:accept阻塞等待客戶端接入
	clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
	printf("連接已經建立,client fd = %d.\n", clifd);

	// 客戶端反覆給服務器發
	while (1)
	{
		info st;
		// 回合中第1步:服務器收
		ret = recv(clifd, &st, sizeof(info), 0);

		// 回合中第2步:服務器解析客戶端數據包,然後幹活,
		if (st.cmd == CMD_REGISTER)
		{
			printf("用戶要註冊學生信息\n");
			printf("學生姓名:%s,學生年齡:%d\n", st.name, st.age);
			// 在這裏服務器要進行真正的註冊動作,一般是插入數據庫一條信息
			
			// 回合中第3步:回覆客戶端
			st.stat = STAT_OK;
			ret = send(clifd, &st, sizeof(info), 0);
		}
		
	}

	return 0;
}

客戶端代碼:client.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERADDR		"192.168.1.104"		// 服務器開放給我們的IP地址和端口號
#define SERPORT		9003

//發送、接收緩衝區
char sendbuf[100];
char recvbuf[100];

#define CMD_REGISTER	1001	// 註冊學生信息
#define CMD_CHECK		1002	// 檢驗學生信息
#define CMD_GETINFO		1003	// 獲取學生信息

#define STAT_OK			30		// 回覆ok
#define STAT_ERR		31		// 回覆出錯了

typedef struct commu
{
	char name[20];		// 學生姓名
	int age;			// 學生年齡
	int cmd;			// 命令碼
	int stat;			// 狀態信息,用來回復
}info;

int main(void)
{
	int sockfd = -1, ret = -1;
	//這個結構體是網絡編程接口中用來表示一個IP地址的,
	//這個IP地址是兼容IPv4和IPv6的
	struct sockaddr_in seraddr = {0};
	struct sockaddr_in cliaddr = {0};
	
	// 第1步:創建socket(AF_INET:使用IPv4進行通信,SOCK_STREAM:TCP)
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("socket");
		return -1;
	}
	printf("socketfd = %d.\n", sockfd);
	
	// 第2步:connect鏈接服務器,向結構體填充服務器信息
	seraddr.sin_family = AF_INET;		// 設置地址族爲IPv4
	seraddr.sin_port = htons(SERPORT);	// 設置地址的端口號信息(檢測大小端並調整)
	seraddr.sin_addr.s_addr = inet_addr(SERADDR);	// 設置IP地址
	ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
	if (ret < 0)
	{
		perror("connect");
		return -1;
	}
	printf("成功建立連接\n");

	while (1)
	{
		// 回合中第1步:客戶端給服務器發送信息
		info st1;
		printf("請輸入學生姓名\n");
		scanf("%s", st1.name);
		printf("請輸入學生年齡");
		scanf("%d", &st1.age);
		st1.cmd = CMD_REGISTER;
		ret = send(sockfd, &st1, sizeof(info), 0);
		printf("已發送%s學生的信息\n",st1.name);
		
		// 回合中第2步:客戶端接收服務器的回覆
		memset(&st1, 0, sizeof(st1));
		ret = recv(sockfd, &st1, sizeof(st1), 0);
		
		// 回合中第3步:客戶端解析服務器的回覆,再做下一步定奪
		if (st1.stat == STAT_OK)
		{
			printf("註冊學生信息成功\n");
		}
		else
		{
			printf("註冊學生信息失敗\n");
		}

	}

	return 0;
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章