《朱老師物聯網大講堂》學習筆記
學習地址: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這些應用層協議,就是這樣來的,不過它們設計的很完善。