《朱老師物聯網大講堂》學習筆記
學習地址:www.zhulaoshi.org
(1).
linux網絡編程框架,
網絡是分層的,OSI是7層的,這種分層是理論的,
實際應用只有4層,TCP/IP,
處理問題時,一定要知道你自己在哪一層,我們目前關注的是應用層,
因為網絡是目前最復雜的通信體系,
曾經有很多類似tcp/ip的協議,
CS,client server,客戶端服務器架構,
比如qq客戶端與qq服務器進行通信,
BS,broswer server,浏覽器服務器架構,
可以把浏覽器理解為一個通用的客戶端,主流的,
(2).
TCP協議傳輸特性,
工作在傳輸層,
對上為socket接口提供服務,對下調用ip層,
面向連接,通信前要先進行3次握手建立連接關系,
tcp好比順豐,傳輸可靠,
tcp如何保證可靠傳輸,
接收方ack給發送方,若發送方未收到ack會重傳,
校驗碼,確保數據未損壞,
滑動窗口技術,來調節網絡適配速率,
給報文編號,若接收方收到的編號錯誤,就會重傳,
(3).
建立連接的條件;服務器listen時客戶端主動發起connect,
TCP的三次握手,雙方之間進行3次單向通信,
SYN,客戶端connect發送SYN,進入SYN-SEND狀態,
SYN+ACK,服務器收到SYN後,服務器端發送SYN-ACK,進入SYN-RCVD狀態,
ACK,客戶端進入establishd,發送ACK,服務器收到ACK後,服務器進入establisted,
上面這幾步驟,沒涉及錯誤的情況,
關閉連接需要4次揮手,
FIN,客戶端FIN,
ACK,服務器ACK,
FIN,服務器FIN,
ACK,客戶端ACK,
上面是客戶端主動關閉,也可以服務器主動關閉,
上面這些握手,揮手都封裝在TCP協議內部,和socket沒關系,
基於TCP通信的服務模式,
具有公網ip地址的服務器,公網ip地址有限,不是每個人都能有一個,那麼可以使用動態公網ip地址映射技術,
服務器端socket,blind,listen後處於監聽狀態,
客戶端端socket後,直接connect去發起連接,
然後雙方就可以建立tcp連接收發數據然後關閉連接,
使用tcp協議的應用,
http,ftp,qq服務器,mail服務器,
(4)
socket編程接口,
建立連接,socket,bind,listen/connect,
int socket(int domain, int type, int protocol);
domain,網絡域,ipv4或者其它類型,
type,SOCK_STREAM指的是tcp連接,SOCK_DGRAM指的是udp連接,
protocol,給0使用默認協議,
返回值int,返回一個套接字,有點像文件描述符,
int bind( int socket, const struct sockaddr *address, socklen_t address_len);
把本地ip地址和我們的socket綁定起來,
socket是上一步得到的,address不區分ipv4和ipv6,address_len表示結構體的長度,
int listen(int socket, int backlog);
socket第一,二步的那個,
backlog指定同時監聽幾個,服務器會有個監聽隊列,
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
address是要連接的服務器的ip地址,socket是之前打開的,
發送和接收,
ssize_t write( int fildes, const void *buf, size_t nbyte);
fildes其實就是socket,
ssize_t send( int socket, const void *buffer, size_t length, int flags );
flag,正常通信用不到,設為0,此時和write差不多,
ssize_t recv(....);
ssize_t read(....);
ip地址十進制形式,點分二進制形式,下面的函數就是負責兩種形式的轉換,
inet_aton,inet_ntoa,inet_addr,
inet_ntop,inet_pton,
n代表網絡net那端使用的二進制形式,p代表字符串也就是255.255.255.255這種形式,
上面3個不支持ipv6,功能差不多,
ip地址相關數據結構都在netinet/in.h,
struct sockaddr,用來表示一個ip地址,兼容ipv6,這個結構體是linux的網絡編程接口中用來表示IP地址的標准結構體,bind、connect等函數中都需要這個結構體,這個結構體是兼容IPV4和IPV6的。在實際編程中這個結構體會被一個struct sockaddr_in或者一個struct sockaddr_in6所填充。
typedef uint32_t in_addr_t; 網絡內部用來表示IP地址的類型,
struct in_addr
{
in_addr_t s_addr;
};
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)];
};
(5)
in_addr_t inet_addr( const char *cp );
把點分十進制格式轉換為二進制格式的ip地址,這個函數同時會轉換為大端模式,
cp = 192.168.1.102,得到0x6601a8c0,數值正確,順序不同,也就是大小端!
那怎麼辦呢?
於是統一規定了一個網絡字節序,其實大端模式,
大端可以這樣記憶,易於機器讀出,因為數據肯定是從低地址開始讀(比如0地址),而數字等是從高位開始辨識(千位肯定比各位先讀出來),
而小端可以這樣記憶,想當然得以為,高位數據放高地址,
int inet_pton(int AF, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
(6).
端口號,區分進程,實質是一個數字編號,會包含在每一個發送的數據包中,
bind就是把當前的ip地址和端口號和socket綁定在一起,
int sockfd = 0;
struct sockaddr_in seraddr
sockfd = socket();
htonl,
htons,
h = host,n = net,
l代表4個字節,s代表兩個字節
寫一部分調試一部分,
(7),(8),(9)
要說的東西主要體現在代碼裡面,
以下代碼為朱老師純手工打造,
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.141" // 服務器開放給我們的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) { // 第1步:先socket打開文件描述符 int sockfd = -1, ret = -1; 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步: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("listen"); return -1; } printf("成功建立連接\n"); /* while (1) { // 回合中第1步:客戶端給服務器發送信息 printf("請輸入要發送的內容\n"); scanf("%s", sendbuf); //printf("剛才輸入的是:%s\n", sendbuf); ret = send(sockfd, sendbuf, strlen(sendbuf), 0); printf("發送了%d個字符\n", ret); // 回合中第2步:客戶端接收服務器的回復 memset(recvbuf, 0, sizeof(recvbuf)); ret = recv(sockfd, recvbuf, sizeof(recvbuf), 0); //printf("成功接收了%d個字節\n", ret); printf("client發送過來的內容是:%s\n", recvbuf); // 回合中第3步:客戶端解析服務器的回復,再做下一步定奪 } */ while (1) { // 回合中第1步:客戶端給服務器發送信息 info st1; printf("請輸入學生姓名\n"); scanf("%s", st1.name); printf("請輸入學生年齡"); scanf("%d", &st1.age); st1.cmd = CMD_REGISTER; //printf("剛才輸入的是:%s\n", sendbuf); ret = send(sockfd, &st1, sizeof(info), 0); printf("發送了1個學生信息\n"); // 回合中第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; }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.141" // 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) { // 第1步:先socket打開文件描述符 int sockfd = -1, ret = -1, clifd = -1; socklen_t len = 0; struct sockaddr_in seraddr = {0}; struct sockaddr_in cliaddr = {0}; char ipbuf[30] = {0}; 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"); // 第三步: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; // 回合中第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); } if (st.cmd == CMD_CHECK) { } if (st.cmd == CMD_GETINFO) { } } return 0; }我們要自己去設計安排應用層,數據收發的規則,
http,ftp這些應用層協議,就是這樣來的,不過它們設計的很完善。