1、BS 和 CS
(1)CS架構:(client server,客戶端服務器架構)
(2)BS架構:(broswer server,瀏覽器服務器架構)
2、關於TCP理解的重點
(1)TCP協議工作在傳輸層,對上服務socket接口,對下調用IP層。
(2)TCP協議面向連接,通信前必須先3次握手建立連接關係後才能開始通信。
(3)TCP協議提供可靠傳輸,不怕丟包、亂序等。
3、TCP如何保證可靠傳輸
(1)TCP在傳輸有效信息前要求通信雙方必須先握手,建立連接才能通信
(2)TCP的接收方收到數據包後會ack給發送方,若發送方未收到ack會丟包重傳
(3)TCP的有效數據內容會附帶校驗,以防止內容在傳遞過程中損壞
(4)TCP會根據網絡帶寬來自動調節適應速率(滑動窗口技術)
(5)發送方會給各分割報文編號,接收方會校驗編號,一旦順序錯誤即會重傳。
4、TCP的三次握手
(1)建立連接需要三次握手
(2)建立連接的條件:服務器listen時客戶端主動發起connect
5、TCP的四次握手(揮手)
(1)關閉連接需要四次握手
(2)服務器或者客戶端都可以主動發起關閉
注:這些握手協議已經封裝在TCP協議內部,socket編程接口平時不用管。
6、基於TCP通信的服務模式
(1)具有公網IP地址的服務器(或者使用動態IP地址映射技術)
(2)服務器端socket、bind、listen後處於監聽狀態
(3)客戶端socket後,直接connect去發起連接
(4)服務器收到並同意客戶端接入後會建立TCP連接,然後雙方開始收發數據,收發時是雙向的,而且雙方均可發起。
(5)雙方均可發起關閉連接。
7、常見的使用了TCP協議的網絡應用
(1)http、ftp
(2)QQ服務器
(3)mail服務器
8、socket編程接口介紹
8.1、建立連接
(1)socket。
socket函數類似於open,用來打開一個網絡連接,如果成功則返回一個網絡文件描述符(int類型),之後我們操作這個網絡連接都通過這個網絡文件描述符。
(2)bind
(3)listen
(4)connect
8.2、發送和接收
(1)send 和 write
(2)recv 和 read
8.3、輔助性函數
(1)inet_aton、 inet_addr、 inet_ntoa
(2)inet_ntop、 inet_pton
8.4、表示IP地址的相關數據結構
(1)都定義在 netinet/in.h
(2)struct socket,
這個結構體是網絡編程接口中用來表示一個IP地址的,注意這個IP地址是不區分IPv4和IPv6的(或者說是兼容IPv4和IPv6的)
(3)typedef uint32_t in_addr_t; 網絡內部用來表示IP地址的類型
(4)struct 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)];
};
(6)struct sockaddr
這個結構體是linux的網絡編程接口中用來表示IP地址的標準結構體,bind、connect等函數中都需要這個結構體,這個結構體是兼用IPv4和IPv6的。在實際編程中這個結構體會被一個 struct sockaddr_in 或者一個 struct sockaddr_in6所填充。
(7)表示IP地址的相關數據結構
類型小結:in_addr_t、 struct in_addr、 struct sockaddr_in、struct sockaddr
9、inet_addr、inet_ntop、 inet_pton函數使用實例:
- inet_addr //IP:點分十進制轉二進制
- inet_ntop //IP: 二進制轉十進制(字符串顯示192.168.xx.xx)
- inet_pton //IP:點分十進制轉二進制 (兼容IPv6的新函數)
#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)
{
struct in_addr addr = {0};
char buf[50] = {0};
addr.s_addr = 0x6601a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf)); //二進制轉十進制(字符串顯示192.168.xx.xx)
printf("ip addr = %s.\n", buf);
/*
//使用inet_pton來轉換
int ret = 0;
char buf[50] = {0};
in_addr_t addr = 0;
//addr = inet_addr(IPADDR); //IP:點分十進制轉二進制
ret = inet_pton( AF_INET, IPADDR, &addr); //IP:點分十進制轉二進制 (兼容IPv6的新函數)
if (ret != 1)
{
printf("inet_pton error.\n");
return -1;
}
printf("addr = 0x%x.\n", addr); //0x6601a8c0 */
return 0;
}
10、socket實踐編程
10.1、服務器端程序編寫
(1)socket
(2)bind
(3)listen
(4)accept
注意:
accept,返回值是一個fd, accept正確返回就表示我們已經和前來連接我的客戶端之間建立了一個TCP連接了,以後我們就要通過這個連接來和客戶端進行讀寫操作,讀寫操作就需要一個fd,這個fd就由accept來返回了。
socket返回的fd叫做監聽fd,是用來監聽客戶端的,不能用來和任何客戶端進行讀寫;
accept返回的fd叫做連接fd,用來和連接的客戶端程序進行讀寫。
10.2、客戶端程序編寫
(1)socket
(2)connect
概念:端口號,實質就是一個數字編號,用來在我們一臺主機中(主機的操作系統中)唯一的標識一個能上網的進程。端口號和IP地址一起會被打包到當前進程發出或者接收到的每一個數據包中。每一個數據包將來在網絡上傳遞的時候,內部都包含了發送方和接收方的信息(就是IP地址和端口號),所以IP地址和端口號這兩個往往打包在一起不分家的。
10.3、
(1)客戶端發送 & 服務器接收
(2)服務器發送 & 客戶端接收
(3)客戶端和服務器原則上都可以任意的發和收,但是實際上雙方必須配合:
- client發的時候server就收,
- 而server發的時候client就收。
(4)必須瞭解到一點:client和server之間的通信是異步的。
(5)解決方案:依靠應用層協議來解決。說白了就是我們server和client事先做好一系列的通信約定。
10.4、自定義應用層協議步驟
(1)自定義應用層協議第一步:規定發送和接收方法
- 規定連接建立後由客戶端主動向服務器發送1個請求數據包,然後服務器收到數據包後回覆客戶端一個迴應數據包,這就是一個通信回合。
- 整個連接的通信就是由N多個回合組成的。
(2)、自定義應用層協議第二步:定義數據包格式
10.5、socket編程實例
實例功能:
(1)編寫服務器、客戶端程序,實現服務器/客戶端之間的TCP通信
(2)自定義應用層協議,實現學生信息註冊、獲取等基本功能, 如:客戶端向服務器提交註冊學生信息的請求,服務器收到請求後,註冊完成並返回註冊成功的信息。
(3)本程序只是走通模擬了這個流程,具體的服務器註冊等功能並未實現,可根據具體業務進行拓展。
服務器端程序:server.c
//server.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <arpa/inet.h>
#include <errno.h>
#include "pubdata.h"
#define SERPORT 6666 //通信端口號
#define SERADDR "192.168.1.108" //IP地址 ubuntu中ifconfig看到的
#define BACKLOG 100
char sendbuf[100] = {0};
char recvbuf[100] = {0};
int main(void)
{
int sockfd = -1, ret = -1, clifd = -1;
socklen_t len = 0;
struct sockaddr_in seraddr = {0};
struct sockaddr_in cliaddr = {0};
//第一步:先socket打開文件描述符
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return -1;
}
printf("socketfd = %d.\n", sockfd);
//第二步:bind綁定sockfd和當前電腦的ip地址&端口號
seraddr.sin_family = AF_INET; //設置地址族爲IPv4
seraddr.sin_port = htons(SERPORT);//htons: host to net short 設置地址的端口號信息
seraddr.sin_addr.s_addr = inet_addr(SERADDR); //設置IP地址
ret = bind(sockfd, ( struct sockaddr *)&seraddr, sizeof(seraddr)); //綁定
if (ret < 0)
{
perror("bind");
return -1;
}
printf("bind success.\n");
//第三步:listen監聽端口
ret = listen(sockfd, BACKLOG);
if (ret < 0)
{
perror("listen");
return -1;
}
//第四步:accept阻塞等待客戶端接入
clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
printf("連接已建立,client fd = %d.\n", clifd);
//連接建立就可以通信了
//服務器端接收
while(1)
{
//定義一個用於描述自定義通信協議的結構體類型info,並定義結構體變量st
info st;
//回合中第一步:服務器收
ret = recv(clifd, &st, sizeof(info), 0);
//回合中的第二步:服務器解析客戶端數據包,然後幹活
if (st.cmd == CMD_REGISTER)
{
printf("用戶要註冊的學生信息\n");
printf("學生姓名:%s, 學生年齡:%d.\n", st.name, st.age);
//在這裏服務器要進行真正的註冊動作,一般是插入數據庫一條信息記錄
//回合中的第三步:回覆客戶端
st.stat = STAT_OK;
ret = send(clifd, &st, sizeof(info), 0);
}
if (st.cmd == CMD_CHECK)
{
//具體動作
}
if (st.cmd == CMD_REGISTER)
{
//具體動作
}
}
return 0;
}
客戶端程序:client.c
//client.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include "pubdata.h"
#define SERPORT 6666 //服務器給客戶端開放的通信端口號
#define SERADDR "192.168.1.108" //服務器給客戶端開放的IP地址
#define BACKLOG 100
char sendbuf[100] = {0};
char recvbuf[100] = {0};
int main(void)
{
int sockfd = -1, ret = -1, clifd = -1;
struct sockaddr_in seraddr = {0};
//第一步:先socket打開文件描述符
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//錯誤檢驗
if (-1 == sockfd)
{
perror("socket");
return -1;
}
printf("socketfd = %d.\n", sockfd);
//第二步: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)
{
//定義一個用於描述自定義通信協議的結構體類型info,並定義結構體變量st1
info st1;
printf("請輸入學生姓名\n");
scanf("%s", st1.name);
printf("請輸入學生年齡\n");
scanf("%d", &st1.age);
st1.cmd = CMD_REGISTER; //使用自定義註冊功能碼CMD_REGISTER
//回合中第一步:客戶端給服務器發送消息
ret = send(sockfd, &st1, sizeof(info), 0);
printf("發送了一個學生信息.\n");
//回合中的第二步:客戶端接收服務器的回覆
memset(&st1, 0, sizeof(st1));
ret = recv(sockfd, &st1, sizeof(st1), 0);
//回合中第三步:客戶端解析服務器的回覆,再做下一步定奪
if (st1.stat == STAT_OK)
{
printf("註冊學生信息成功.\n");
}
else
{
printf("註冊學生信息失敗.\n");
}
}
return 0;
}
定義數據包格式:
用於描述自定義應用層協議的公用數據結構信息(包含在pubdata.h頭文件中):
pubdata.h
//pubdata.h
#ifndef __PUBDATA_H
#define __PUBDATA_H
#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;
#endif
上述server.c和client.c程序通過編譯鏈接後,生成兩個可執行程序,如:server和client,
首先運行服務器端server,
然後運行客戶端client,
正常情況下,雙方即成功建立TCP通信,
功能驗證:
客戶端向服務器端提交學生信息,
服務器收到客戶端請求後則會主動回覆信息,
如下:
客戶端:
服務器端:
出現上述結果則表明客戶端和服務器端之間通信成功,且定義的數據包格式亦能正常工作,即:
(1)規定連接建立後由客戶端主動向服務器發送1個請求數據包,然後服務器收到數據包後回覆客戶端一個迴應數據包,這就是一個通信回合。
(2)整個連接的通信就是由N多個回合組成的。
注:學習資料參考《朱老師物聯網大講堂》——linux網絡編程實踐