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理解的重點
- TCP協議工作在OSI七層模型的傳輸層,對上服務socket接口,對下調用IP層.
- TCP協議面向連接,通信前必須先3次握手建立連接關係後才能開始通信。
- TCP協議提供可靠傳輸,不怕丟包、亂序等。
2、TCP如何保證可靠傳輸
- TCP在傳輸有效信息前要求通信雙方必須先握手,建立連接才能通信
- TCP的接收方收到數據包後會ack給發送方,若發送方未收到ack會丟包重傳
- TCP的有效數據內容會附帶校驗,以防止內容在傳遞過程中損壞
- TCP會根據網絡帶寬來自動調節適配速率(滑動窗口技術)
- 發送方會給各分割報文編號,接收方會校驗編號,一旦順序錯誤即會重傳。
建立TCP需要三次握手才能建立,而斷開連接則需要四次握手。整個過程如下圖所示:
3、TCP的三次握手
- 首先Client端發送連接請求報文,Server段接受連接後回覆ACK報文,併爲這次連接分配資源。Client端接收到ACK報文後也向Server段發生ACK報文,並分配資源,這樣TCP連接就建立了。
- 建立連接的條件:服務器listen時客戶端主動發起connect
4、TCP的四次揮手
- 斷開連接的條件:服務器或者客戶端都可以主動發起關閉。
- 斷開過程:假設客戶端先向其TCP發出連接釋放報文段,並停止再發送數據,主動關閉TCP連接。客戶端發送釋放報文FIN [第一次] 。服務器收到釋放報文後發出確認報文ACK [第二次] 。服務器發出的連接釋放報文FIN,並且還附帶上次已發送過的確認號ACK [第三次] 。客戶端在收到服務器的連接釋放報文段後,發送確認報文ACK [第四次] 。
注:這些握手協議已經封裝在TCP協議內部,socket編程接口平時不用管
5、基於TCP通信的服務模式
- 具有公網IP地址的服務器(或者使用動態IP地址映射技術)
- 服務器端socket、bind、listen後處於監聽狀態
- 客戶端socket後,直接connect去發起連接。
- 服務器收到並同意客戶端接入後會建立TCP連接,然後雙方開始收發數據,收發時是雙向的,而且雙方均可發起
- 雙方均可發起關閉連接
- 常見的使用了TCP協議的網絡應用:http(相當於一個應用程序,用來傳輸文本信息)、ftp、QQ服務器和mail服務器。這些需要很高可靠的應用,底層都是基於TCP協議的。
二,socket編程接口介紹
1、建立連接
- socket:socket函數類似於open,用來打開一個網絡連接,如果成功則返回一個網絡文件描述符(int類型),之後我們操作這個網絡連接都通過這個網絡文件描述符。
- == bind==:用來進行綁定的函數,把本地的IP地址和socket進行綁定。功能類似於fctrl函數,是用來改變屬性的函數。
- listen:監聽一個端口,監聽的是在bind時綁定的那個地址。
- accept:返回值是一個fd,accept正確返回就表示我們已經和前來連接我的客戶端之間建立了一個TCP連接了,以後我們就要通過這個連接來和客戶端進行讀寫操作,讀寫操作就需要一個fd,這個fd就由accept來返回了。(阻塞的位置)
- connect:用來連接服務器的(客戶端那邊用)。
2、發送和接收
- send/write:發送數據。(send比write就多了一個flag,只有支持一些特殊協議時會用到flag,普通情況寫0即可)
- recv/read:接收數據。(在網絡中發送有點像是寫文件,在網絡中接收有點像是收文件)
3、輔助性函數
- ==inet_aton、inet_addr、inet_ntoa ==:(點分十進制和32位二進制形式互相轉化,不支持IPv6)
- inet_ntop、inet_pton:(點分十進制和32位二進制形式互相轉化,支持IPv6)
4、表示IP地址相關數據結構
-
都定義在 netinet/in.h
-
struct sockaddr,這個結構體是網絡編程接口中用來表示一個IP地址的,注意這個IP地址是兼容IPv4和IPv6的
-
typedef uint32_t in_addr_t; 網絡內部用來表示IP地址的類型
-
struct in_addr
struct in_addr { in_addr_t s_addr; };
-
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)];
};
- 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;
}