01P-復習-Linux網絡編程
02P-信號量生產者復習
03P-協議 協議: 一組規則。
04P-7層模型和4層模型及代表協議 分層模型結構:
OSI七層模型: 物、數、網、傳、會、表、應TCP/IP 4層模型:網(鏈路層/網絡接口層)、網、傳、應應用層:http、ftp、nfs、ssh、telnet。。。傳輸層:TCP、UDP網絡層:IP、ICMP、IGMP鏈路層:以太網幀協議、ARP
05P-網絡傳輸數據封裝流程
網絡傳輸流程:
數據沒有封裝之前,是不能在網絡中傳遞。數據-》應用層-》傳輸層-》網絡層-》鏈路層 --- 網絡環境
06P-以太網幀和ARP請求
以太網幀協議:
ARP協議:根據 Ip 地址獲取 mac 地址。以太網幀協議:根據mac地址,完成數據包傳輸。
07P-IP協議 IP協議:
版本: IPv4、IPv6 -- 4位TTL: time to live 。 設置數據包在路由節點中的跳轉上限。每經過一個路由節點,該值-1, 減為0的路由,有義務將該數據包丟棄源IP: 32位。--- 4字節 192.168.1.108 --- 點分十進制 IP地址(string) --- 二進制 目的IP:32位。--- 4字節
08P-端口號和UDP協議 UDP: 16位:源端口號。 2^16 = 65536
16位:目的端口號。
IP地址:可以在網絡環境中,唯一標識一臺主機。
端口號:可以網絡的一臺主機上,唯一標識一個進程。
ip地址+端口號:可以在網絡環境中,唯一標識一個進程。
09P-TCP協議 TCP協議:
16位:源端口號。 2^16 = 65536 16位:目的端口號。32序號;32確認序號。 6個標志位。16位窗口大小。 2^16 = 65536
10P-BS和CS模型對比 c/s模型:
client-server
b/s模型:
browser-serverC/S B/S優點: 緩存大量數據、協議選擇靈活 安全性、跨平臺、開發工作量較小速度快缺點: 安全性、跨平臺、開發工作量較大 不能緩存大量數據、嚴格遵守 http
11P-套接字 網絡套接字: socket
一個文件描述符指向一個套接字(該套接字內部由內核借助兩個緩沖區實現。)在通信過程中, 套接字一定是成對出現的。
12P-回顧
13P-網絡字節序 網絡字節序:
小端法:(pc本地存儲) 高位存高地址。地位存低地址。 int a = 0x12345678大端法:(網絡存儲) 高位存低地址。地位存高地址。htonl --> 本地--》網絡 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 網絡字節序htons --> 本地--》網絡 (port)ntohl --> 網絡--》 本地(IP)ntohs --> 網絡--》 本地(Port)
14P-IP地址轉換函數 IP地址轉換函數:
int inet_pton(int af, const char *src, void *dst); 本地字節序(string IP) ---> 網絡字節序af:AF_INET、AF_INET6src:傳入,IP地址(點分十進制)dst:傳出,轉換后的 網絡字節序的 IP地址。 返回值:成功: 1異常: 0, 說明src指向的不是一個有效的ip地址。失敗:-1const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 網絡字節序 ---> 本地字節序(string IP)af:AF_INET、AF_INET6src: 網絡字節序IP地址dst:本地字節序(string IP)size: dst 的大小。返回值: 成功:dst。 失敗:NULL
15P-sockaddr地址結構
sockaddr地址結構: IP + port --> 在網絡環境中唯一標識一個進程。
struct sockaddr_in addr;addr.sin_family = AF_INET/AF_INET6 man 7 ipaddr.sin_port = htons(9527);int dst;inet_pton(AF_INET, "192.157.22.45", (void *)&dst);addr.sin_addr.s_addr = dst;【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系統中有效的任意IP地址。二進制類型。bind(fd, (struct sockaddr *)&addr, size);
16P-socket模型創建流程分析
17P-socket和bind socket函數:
#include <sys/socket.h>int socket(int domain, int type, int protocol); 創建一個 套接字domain:AF_INET、AF_INET6、AF_UNIXtype:SOCK_STREAM、SOCK_DGRAMprotocol: 0 返回值:成功: 新套接字所對應文件描述符失敗: -1 errno
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 給socket綁定一個 地址結構 (IP+port)sockfd: socket 函數返回值struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = htonl(INADDR_ANY);addr: 傳入參數(struct sockaddr *)&addraddrlen: sizeof(addr) 地址結構的大小。返回值:成功:0失敗:-1 errno
18P-listen和accept int listen(int sockfd, int backlog); 設置同時與服務器建立連接的上限數。(同時進行3次握手的客戶端數量)
sockfd: socket 函數返回值backlog:上限數值。最大值 128.返回值:成功:0失敗:-1 errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客戶端建立連接,成功的話,返回一個與客戶端成功連接的socket文件描述符。
sockfd: socket 函數返回值addr:傳出參數。成功與服務器建立連接的那個客戶端的地址結構(IP+port)socklen_t clit_addr_len = sizeof(addr);addrlen:傳入傳出。 &clit_addr_len入:addr的大小。 出:客戶端addr實際大小。返回值:成功:能與客戶端進行數據通信的 socket 對應的文件描述。失敗: -1 , errno
19P-connect int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用現有的 socket 與服務器建立連接
sockfd: socket 函數返回值struct sockaddr_in srv_addr; // 服務器地址結構srv_addr.sin_family = AF_INET;srv_addr.sin_port = 9527 跟服務器bind時設定的 port 完全一致。inet_pton(AF_INET, "服務器的IP地址",&srv_adrr.sin_addr.s_addr);addr:傳入參數。服務器的地址結構addrlen:服務器的地址結構的大小返回值:成功:0失敗:-1 errno如果不使用bind綁定客戶端地址結構, 采用"隱式綁定".
20P-CS模型的TCP通信分析 TCP通信流程分析:
server:1. socket() 創建socket2. bind() 綁定服務器地址結構3. listen() 設置監聽上限4. accept() 阻塞監聽客戶端連接5. read(fd) 讀socket獲取客戶端數據6. 小--大寫 toupper()7. write(fd)8. close();client:1. socket() 創建socket2. connect(); 與服務器建立連接3. write() 寫數據到 socket4. read() 讀轉換后的數據。5. 顯示讀取結果6. close()
21P-server的實現 代碼如下:
#include <stdio.h> #include <ctype.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #define SERV_PORT 9527 void sys_err(const char *str) { perror(str);
exit(1);
} int main(int argc, char *argv[]) { int lfd = 0, cfd = 0;
int ret, i;
char buf[BUFSIZ], client_IP[1024];
struct sockaddr_in serv_addr, clit_addr; // 定義服務器地址結構 和 客戶端地址結構
socklen_t clit_addr_len; // 客戶端地址結構大小
serv_addr.sin_family = AF_INET; // IPv4
serv_addr.sin_port = htons(SERV_PORT); // 轉為網絡字節序的 端口號
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 獲取本機任意有效IP
lfd = socket(AF_INET, SOCK_STREAM, 0); //創建一個 socket
if (lfd == -1) {
sys_err("socket error");
}
bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//給服務器socket綁定地址結構(IP+port)
listen(lfd, 128); // 設置監聽上限
clit_addr_len = sizeof(clit_addr); // 獲取客戶端地址結構大小
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); // 阻塞等待客戶端連接請求
if (cfd == -1)
sys_err("accept error");
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),
ntohs(clit_addr.sin_port)); // 根據accept傳出參數,獲取客戶端 ip 和 port
while (1) {
ret = read(cfd, buf, sizeof(buf)); // 讀客戶端數據
write(STDOUT_FILENO, buf, ret); // 寫到屏幕查看
for (i = 0; i < ret; i++) // 小寫 -- 大寫
buf[i] = toupper(buf[i]);
write(cfd, buf, ret); // 將大寫,寫回給客戶端。
}
close(lfd);
close(cfd);
return 0;
}
編譯測試,結果如下:
22P-獲取客戶端地址結構 cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); accept函數中的clit_addr傳出的就是客戶端地址結構,IP+port
于是,在代碼中增加此段代碼,可獲取客戶端信息: printf(“client ip:%s port:%d\n”, inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)), ntohs(clit_addr.sin_port));
上一節代碼中已經有這段代碼,這里就不再跑一遍了。
23P-client的實現
#include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #define SERV_PORT 9527 void sys_err(const char *str) { perror(str);
exit(1);
} int main(int argc, char *argv[]) { int cfd;
int conter = 10;
char buf[BUFSIZ];
struct sockaddr_in serv_addr; //服務器地址結構
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
//inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
sys_err("socket error");
int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (ret != 0)
sys_err("connect err");
while (--conter) {
write(cfd, "hello\n", 6);
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
sleep(1);
}
close(cfd);
return 0;
}
編譯運行,結果如下:
這里遇到過一個問題,如果之前運行server,用Ctrl+z終止進程,ps aux列表里會有服務器進程殘留,這個會影響當前服務器。解決方法是kill掉這些服務器進程。不然端口被占用,當前運行的服務器進程接收不到東西,沒有回顯。
24P-總結
協議: 一組規則。
分層模型結構:
OSI七層模型: 物、數、網、傳、會、表、應TCP/IP 4層模型:網(鏈路層/網絡接口層)、網、傳、應應用層:http、ftp、nfs、ssh、telnet。。。傳輸層:TCP、UDP網絡層:IP、ICMP、IGMP鏈路層:以太網幀協議、ARP
c/s模型:
client-server
b/s模型:
browser-serverC/S B/S優點: 緩存大量數據、協議選擇靈活 安全性、跨平臺、開發工作量較小速度快缺點: 安全性、跨平臺、開發工作量較大 不能緩存大量數據、嚴格遵守 http
網絡傳輸流程:
數據沒有封裝之前,是不能在網絡中傳遞。數據-》應用層-》傳輸層-》網絡層-》鏈路層 --- 網絡環境
以太網幀協議:
ARP協議:根據 Ip 地址獲取 mac 地址。以太網幀協議:根據mac地址,完成數據包傳輸。
IP協議:
版本: IPv4、IPv6 -- 4位TTL: time to live 。 設置數據包在路由節點中的跳轉上限。每經過一個路由節點,該值-1, 減為0的路由,有義務將該數據包丟棄源IP: 32位。--- 4字節 192.168.1.108 --- 點分十進制 IP地址(string) --- 二進制 目的IP:32位。--- 4字節
IP地址:可以在網絡環境中,唯一標識一臺主機。
端口號:可以網絡的一臺主機上,唯一標識一個進程。
ip地址+端口號:可以在網絡環境中,唯一標識一個進程。
UDP: 16位:源端口號。 2^16 = 65536
16位:目的端口號。
TCP協議:
16位:源端口號。 2^16 = 65536 16位:目的端口號。32序號;32確認序號。 6個標志位。16位窗口大小。 2^16 = 65536
網絡套接字: socket
一個文件描述符指向一個套接字(該套接字內部由內核借助兩個緩沖區實現。)在通信過程中, 套接字一定是成對出現的。
網絡字節序:
小端法:(pc本地存儲) 高位存高地址。地位存低地址。 int a = 0x12345678大端法:(網絡存儲) 高位存低地址。地位存高地址。htonl --> 本地--》網絡 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 網絡字節序htons --> 本地--》網絡 (port)ntohl --> 網絡--》 本地(IP)ntohs --> 網絡--》 本地(Port)
IP地址轉換函數:
int inet_pton(int af, const char *src, void *dst); 本地字節序(string IP) ---> 網絡字節序af:AF_INET、AF_INET6src:傳入,IP地址(點分十進制)dst:傳出,轉換后的 網絡字節序的 IP地址。 返回值:成功: 1異常: 0, 說明src指向的不是一個有效的ip地址。失敗:-1const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 網絡字節序 ---> 本地字節序(string IP)af:AF_INET、AF_INET6src: 網絡字節序IP地址dst:本地字節序(string IP)size: dst 的大小。返回值: 成功:dst。 失敗:NULL
sockaddr地址結構: IP + port --> 在網絡環境中唯一標識一個進程。
struct sockaddr_in addr;addr.sin_family = AF_INET/AF_INET6 man 7 ipaddr.sin_port = htons(9527);int dst;inet_pton(AF_INET, "192.157.22.45", (void *)&dst);addr.sin_addr.s_addr = dst;【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系統中有效的任意IP地址。二進制類型。bind(fd, (struct sockaddr *)&addr, size);
socket函數:
#include <sys/socket.h>int socket(int domain, int type, int protocol); 創建一個 套接字domain:AF_INET、AF_INET6、AF_UNIXtype:SOCK_STREAM、SOCK_DGRAMprotocol: 0 返回值:成功: 新套接字所對應文件描述符失敗: -1 errno#include <arpa/inet.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 給socket綁定一個 地址結構 (IP+port)sockfd: socket 函數返回值struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = htonl(INADDR_ANY);addr: 傳入參數(struct sockaddr *)&addraddrlen: sizeof(addr) 地址結構的大小。返回值:成功:0失敗:-1 errnoint listen(int sockfd, int backlog); 設置同時與服務器建立連接的上限數。(同時進行3次握手的客戶端數量)sockfd: socket 函數返回值backlog:上限數值。最大值 128.返回值:成功:0失敗:-1 errno int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客戶端建立連接,成功的話,返回一個與客戶端成功連接的socket文件描述符。sockfd: socket 函數返回值addr:傳出參數。成功與服務器建立連接的那個客戶端的地址結構(IP+port)socklen_t clit_addr_len = sizeof(addr);addrlen:傳入傳出。 &clit_addr_len入:addr的大小。 出:客戶端addr實際大小。返回值:成功:能與客戶端進行數據通信的 socket 對應的文件描述。失敗: -1 , errnoint connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用現有的 socket 與服務器建立連接sockfd: socket 函數返回值struct sockaddr_in srv_addr; // 服務器地址結構srv_addr.sin_family = AF_INET;srv_addr.sin_port = 9527 跟服務器bind時設定的 port 完全一致。inet_pton(AF_INET, "服務器的IP地址",&srv_adrr.sin_addr.s_addr);addr:傳入參數。服務器的地址結構addrlen:服務器的地址結構的大小返回值:成功:0失敗:-1 errno如果不使用bind綁定客戶端地址結構, 采用"隱式綁定".
TCP通信流程分析:
server:1. socket() 創建socket2. bind() 綁定服務器地址結構3. listen() 設置監聽上限4. accept() 阻塞監聽客戶端連接5. read(fd) 讀socket獲取客戶端數據6. 小--大寫 toupper()7. write(fd)8. close();client:1. socket() 創建socket2. connect(); 與服務器建立連接3. write() 寫數據到 socket4. read() 讀轉換后的數據。5. 顯示讀取結果6. close()
25P-復習
26P-三次握手建立連接
27P-數據通信
并不是一次發送,一次應答。也可以批量應答
28P-四次握手關閉連接
29P-半關閉補充說明 這里其實就是想說明,完成兩次揮手后,不是說兩端的連接斷開了,主動端關閉了寫緩沖區,不能再向對端發送數據,被動端關閉了讀緩沖區,不能再從對端讀取數據。然而主動端還是能夠讀取對端發來的數據。
30P-滑動窗口和TCP數據包格式
滑動窗口:
發送給連接對端,本端的緩沖區大小(實時),保證數據不會丟失。
31P-通信時序與代碼對應關系
32P-TCP通信時序總結 三次握手:
主動發起連接請求端,發送 SYN 標志位,請求建立連接。 攜帶序號號、數據字節數(0)、滑動窗口大小。被動接受連接請求端,發送 ACK 標志位,同時攜帶 SYN 請求標志位。攜帶序號、確認序號、數據字節數(0)、滑動窗口大小。主動發起連接請求端,發送 ACK 標志位,應答服務器連接請求。攜帶確認序號。
四次揮手:
主動關閉連接請求端, 發送 FIN 標志位。 被動關閉連接請求端, 應答 ACK 標志位。 ----- 半關閉完成。被動關閉連接請求端, 發送 FIN 標志位。主動關閉連接請求端, 應答 ACK 標志位。 ----- 連接全部關閉
滑動窗口:
發送給連接對端,本端的緩沖區大小(實時),保證數據不會丟失。
33P-錯誤處理函數的封裝思路 wrap.h文件如下,就是包裹函數的聲明
#ifndef _WRAP_H #define _WRAP_H void perr_exit(const char *s); int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr); int Bind(int fd, const struct sockaddr *sa, socklen_t salen); int Connect(int fd, const struct sockaddr *sa, socklen_t salen); int Listen(int fd, int backlog); int Socket(int family, int type, int protocol); ssize_t Read(int fd, void *ptr, size_t nbytes); ssize_t Write(int fd, const void *ptr, size_t nbytes); int Close(int fd); ssize_t Readn(int fd, void *vptr, size_t n); ssize_t Writen(int fd, const void *vptr, size_t n); ssize_t my_read(int fd, char *ptr); ssize_t Readline(int fd, void *vptr, size_t maxlen); #endif
wrap.c隨便取一部分,如下,就是包裹函數的代碼:
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> void perr_exit(const char *s) { perror(s);
exit(-1);
} int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) { int n;
again: if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return n;
} int Bind(int fd, const struct sockaddr *sa, socklen_t salen) { int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
這里原函數和包裹函數的函數名差異只有首字母大寫,這是因為man page對字母大小寫不敏感,同名的包裹函數一樣可以跳轉至man page
34P-錯誤處理函數封裝 就是重新包裹需要檢查返回值的函數,讓代碼不那么肥胖。
35P-封裝思想總結和readn、readline封裝思想說明 錯誤處理函數:
封裝目的: 在 server.c 編程過程中突出邏輯,將出錯處理與邏輯分開,可以直接跳轉man手冊?!緒rap.c】 【wrap.h】存放網絡通信相關常用 自定義函數 存放 網絡通信相關常用 自定義函數原型(聲明)。命名方式:系統調用函數首字符大寫, 方便查看man手冊如:Listen()、Accept();函數功能:調用系統調用函數,處理出錯場景。在 server.c 和 client.c 中調用 自定義函數聯合編譯 server.c 和 wrap.c 生成 serverclient.c 和 wrap.c 生成 client
readn: 讀 N 個字節
readline:
讀一行
36P-中午復習 三次握手:
主動發起連接請求端,發送 SYN 標志位,請求建立連接。 攜帶序號號、數據字節數(0)、滑動窗口大小。被動接受連接請求端,發送 ACK 標志位,同時攜帶 SYN 請求標志位。攜帶序號、確認序號、數據字節數(0)、滑動窗口大小。主動發起連接請求端,發送 ACK 標志位,應答服務器連接請求。攜帶確認序號。
四次揮手:
主動關閉連接請求端, 發送 FIN 標志位。 被動關閉連接請求端, 應答 ACK 標志位。 ----- 半關閉完成。被動關閉連接請求端, 發送 FIN 標志位。主動關閉連接請求端, 應答 ACK 標志位。 ----- 連接全部關閉
滑動窗口:
發送給連接對端,本端的緩沖區大小(實時),保證數據不會丟失。
錯誤處理函數:
封裝目的: 在 server.c 編程過程中突出邏輯,將出錯處理與邏輯分開,可以直接跳轉man手冊?!緒rap.c】 【wrap.h】存放網絡通信相關常用 自定義函數 存放 網絡通信相關常用 自定義函數原型(聲明)。命名方式:系統調用函數首字符大寫, 方便查看man手冊如:Listen()、Accept();函數功能:調用系統調用函數,處理出錯場景。在 server.c 和 client.c 中調用 自定義函數聯合編譯 server.c 和 wrap.c 生成 serverclient.c 和 wrap.c 生成 client
readn: 讀 N 個字節
readline:
讀一行
read 函數的返回值:
1. > 0 實際讀到的字節數2. = 0 已經讀到結尾(對端已經關閉)【 !重 !點 !】3. -1 應進一步判斷errno的值:errno = EAGAIN or EWOULDBLOCK: 設置了非阻塞方式 讀。 沒有數據到達。 errno = EINTR 慢速系統調用被 中斷。errno = “其他情況” 異常。
37P-多進程并發服務器思路分析 1. Socket(); 創建 監聽套接字 lfd 2. Bind() 綁定地址結構 Strcut scokaddr_in addr; 3. Listen(); 4. while (1) {
cfd = Accpet(); 接收客戶端連接請求。pid = fork();if (pid == 0){ 子進程 read(cfd) --- 小-》大 --- write(cfd)close(lfd) 關閉用于建立連接的套接字 lfdread()小--大write()} else if (pid > 0) { close(cfd); 關閉用于與客戶端通信的套接字 cfd contiue;}}5. 子進程:close(lfd)read()小--大write() 父進程:close(cfd);注冊信號捕捉函數: SIGCHLD在回調函數中, 完成子進程回收while (waitpid());
38P-多線程并發服務器分析 多線程并發服務器: server.c
1. Socket(); 創建 監聽套接字 lfd2. Bind() 綁定地址結構 Strcut scokaddr_in addr;3. Listen(); 4. while (1) { cfd = Accept(lfd, );pthread_create(&tid, NULL, tfn, (void *)cfd);pthread_detach(tid); // pthead_join(tid, void **); 新線程---專用于回收子線程。}5. 子線程:void *tfn(void *arg) {// close(lfd) 不能關閉。 主線程要使用lfdread(cfd)小--大write(cfd)pthread_exit((void *)10); }
39P-多進程并發服務器實現 第一個版本的代碼如下:
#include <stdio.h> #include <ctype.h> #include <stdlib.h> #include <sys/wait.h> #include <string.h> #include <strings.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <sys/socket.h> #include <arpa/inet.h> #include <pthread.h> #include “wrap.h” #define SRV_PORT 9999 int main(int argc, char *argv[]) { int lfd, cfd;
pid_t pid;
struct sockaddr_in srv_addr, clt_addr;
socklen_t clt_addr_len;
char buf[BUFSIZ];
int ret, i;
//memset(&srv_addr, 0, sizeof(srv_addr)); // 將地址結構清零
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(SRV_PORT);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = Socket(AF_INET, SOCK_STREAM, 0);
Bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
Listen(lfd, 128);
clt_addr_len = sizeof(clt_addr);
while (1) {
cfd = Accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
pid = fork();
if (pid < 0) {
perr_exit("fork error");
} else if (pid == 0) {
close(lfd);
break;
} else {
close(cfd);
continue;
}
}
if (pid == 0) {
for (;;) {
ret = Read(cfd, buf, sizeof(buf));
if (ret == 0) {
close(cfd);
exit(1);
}
for (i = 0; i < ret; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, ret);
write(STDOUT_FILENO, buf, ret);
}
}
return 0;
}
編譯運行,結果如下:
這個代碼,有問題。我們Ctrl+C終止一個連接進程,會發現,有僵尸進程。
如上圖所示,有個僵尸進程。這是因為父進程在阻塞等待,沒來得及去回收這個子進程。
所以需要修改代碼,增加子進程回收,用信號捕捉來實現。 修改部分如圖所示:
完整代碼如下:
#include <stdio.h> #include <ctype.h> #include <stdlib.h> #include <sys/wait.h> #include <string.h> #include <strings.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <sys/socket.h> #include <arpa/inet.h> #include <pthread.h> #include “wrap.h” #define SRV_PORT 9999 void catch_child(int signum) { while ((waitpid(0, NULL, WNOHANG)) > 0);
return ;
} int main(int argc, char *argv[]) { int lfd, cfd;
pid_t pid;
struct sockaddr_in srv_addr, clt_addr;
socklen_t clt_addr_len;
char buf[BUFSIZ];
int ret, i;
//memset(&srv_addr, 0, sizeof(srv_addr)); // 將地址結構清零
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(SRV_PORT);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = Socket(AF_INET, SOCK_STREAM, 0);
Bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
Listen(lfd, 128);
clt_addr_len = sizeof(clt_addr);
while (1) {
cfd = Accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
pid = fork();
if (pid < 0) {
perr_exit("fork error");
} else if (pid == 0) {
close(lfd);
break;
} else {
struct sigaction act;
act.sa_handler = catch_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
ret = sigaction(SIGCHLD, &act, NULL);
if (ret != 0) {
perr_exit("sigaction error");
}
close(cfd);
continue;
}
}
if (pid == 0) {
for (;;) {
ret = Read(cfd, buf, sizeof(buf));
if (ret == 0) {
close(cfd);
exit(1);
}
for (i = 0; i < ret; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, ret);
write(STDOUT_FILENO, buf, ret);
}
}
return 0;
}
這樣,當子進程退出時,父進程收到信號,就會去回收子進程了,不會出現僵尸進程。
40P-多進程服務器測試IP地址調整 使用橋接模式,讓自己主機和其他人主機處于同一個網段
41P-服務器程序上傳外網服務器并訪問 scp -r 命令,將本地文件拷貝至遠程服務器上目標位置 scp -r 源地址 目標地址
42P-多線程服務器代碼review 代碼如下:
#include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <ctype.h> #include <unistd.h> #include <fcntl.h> #include “wrap.h” #define MAXLINE 8192 #define SERV_PORT 8000 struct s_info { //定義一個結構體, 將地址結構跟cfd捆綁 struct sockaddr_in cliaddr;
int connfd;
}; void *do_work(void *arg) { int n,i;
struct s_info *ts = (struct s_info*)arg;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用"[+d"查看
while (1) {
n = Read(ts->connfd, buf, MAXLINE); //讀客戶端
if (n == 0) {
printf("the client %d closed...\n", ts->connfd);
break; //跳出循環,關閉cfd
}
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
ntohs((*ts).cliaddr.sin_port)); //打印客戶端信息(IP/PORT)
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]); //小寫-->大寫
Write(STDOUT_FILENO, buf, n); //寫出至屏幕
Write(ts->connfd, buf, n); //回寫給客戶端
}
Close(ts->connfd);
return (void *)0;
} int main(void) { struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
pthread_t tid;
struct s_info ts[256]; //創建結構體數組.
int i = 0;
listenfd = Socket(AF_INET, SOCK_STREAM, 0); //創建一個socket, 得到lfd
bzero(&servaddr, sizeof(servaddr)); //地址結構清零
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP
servaddr.sin_port = htons(SERV_PORT); //指定端口號
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //綁定
Listen(listenfd, 128); //設置同一時刻鏈接服務器上限數
printf("Accepting client connect ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //阻塞監聽客戶端鏈接請求
ts[i].cliaddr = cliaddr;
ts[i].connfd = connfd;
pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
pthread_detach(tid); //子線程分離,防止僵線程產生.
i++;
}
return 0;
}
編譯運行,結果如下:
43P-read返回值和總結
三次握手:
主動發起連接請求端,發送 SYN 標志位,請求建立連接。 攜帶序號號、數據字節數(0)、滑動窗口大小。被動接受連接請求端,發送 ACK 標志位,同時攜帶 SYN 請求標志位。攜帶序號、確認序號、數據字節數(0)、滑動窗口大小。主動發起連接請求端,發送 ACK 標志位,應答服務器連接請求。攜帶確認序號。
四次揮手:
主動關閉連接請求端, 發送 FIN 標志位。 被動關閉連接請求端, 應答 ACK 標志位。 ----- 半關閉完成。被動關閉連接請求端, 發送 FIN 標志位。主動關閉連接請求端, 應答 ACK 標志位。 ----- 連接全部關閉
滑動窗口:
發送給連接對端,本端的緩沖區大小(實時),保證數據不會丟失。
錯誤處理函數:
封裝目的: 在 server.c 編程過程中突出邏輯,將出錯處理與邏輯分開,可以直接跳轉man手冊?!緒rap.c】 【wrap.h】存放網絡通信相關常用 自定義函數 存放 網絡通信相關常用 自定義函數原型(聲明)。命名方式:系統調用函數首字符大寫, 方便查看man手冊如:Listen()、Accept();函數功能:調用系統調用函數,處理出錯場景。在 server.c 和 client.c 中調用 自定義函數聯合編譯 server.c 和 wrap.c 生成 serverclient.c 和 wrap.c 生成 client
readn: 讀 N 個字節
readline:
讀一行
read 函數的返回值:
1. > 0 實際讀到的字節數2. = 0 已經讀到結尾(對端已經關閉)【 !重 !點 !】3. -1 應進一步判斷errno的值:errno = EAGAIN or EWOULDBLOCK: 設置了非阻塞方式 讀。 沒有數據到達。 errno = EINTR 慢速系統調用被 中斷。errno = “其他情況” 異常。
多進程并發服務器:server.c
1. Socket(); 創建 監聽套接字 lfd
2. Bind() 綁定地址結構 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {cfd = Accpet(); 接收客戶端連接請求。pid = fork();if (pid == 0){ 子進程 read(cfd) --- 小-》大 --- write(cfd)close(lfd) 關閉用于建立連接的套接字 lfdread()小--大write()} else if (pid > 0) { close(cfd); 關閉用于與客戶端通信的套接字 cfd contiue;}}5. 子進程:close(lfd)read()小--大write() 父進程:close(cfd);注冊信號捕捉函數: SIGCHLD在回調函數中, 完成子進程回收while (waitpid());
多線程并發服務器: server.c
1. Socket(); 創建 監聽套接字 lfd2. Bind() 綁定地址結構 Strcut scokaddr_in addr;3. Listen(); 4. while (1) { cfd = Accept(lfd, );pthread_create(&tid, NULL, tfn, (void *)cfd);pthread_detach(tid); // pthead_join(tid, void **); 新線程---專用于回收子線程。}5. 子線程:void *tfn(void *arg) {// close(lfd) 不能關閉。 主線程要使用lfdread(cfd)小--大write(cfd)pthread_exit((void *)10); }
44P-復習
45P-TCP狀態-主動發起連接 46P-TCP狀態-主動關閉連接 47P-TCP狀態-被動接收連接 48P-TCP狀態-被動關閉連接 49P-2MSL時長 50P-TCP狀態-其他狀態
netstat -apn | grep client 查看客戶端網絡連接狀態 netstat -apn | grep port 查看端口的網絡連接狀態
TCP狀態時序圖:
結合三次握手、四次揮手 理解記憶。1. 主動發起連接請求端: CLOSE -- 發送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 發送 ACK -- ESTABLISHED(數據通信態)2. 主動關閉連接請求端: ESTABLISHED(數據通信態) -- 發送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半關閉)-- 接收對端發送 FIN -- FIN_WAIT_2(半關閉)-- 回發ACK -- TIME_WAIT(只有主動關閉連接方,會經歷該狀態)-- 等 2MSL時長 -- CLOSE 3. 被動接收連接請求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 發送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(數據通信態)4. 被動關閉連接請求端: ESTABLISHED(數據通信態) -- 接收 FIN -- ESTABLISHED(數據通信態) -- 發送ACK -- CLOSE_WAIT (說明對端【主動關閉連接端】處于半關閉狀態) -- 發送FIN -- LAST_ACK -- 接收ACK -- CLOSE重點記憶: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)netstat -apn | grep 端口號
2MSL時長:
一定出現在【主動關閉連接請求端】。 --- 對應 TIME_WAIT 狀態。保證,最后一個 ACK 能成功被對端接收。(等待期間,對端沒收到我發的ACK,對端會再次發送FIN請求。)
51P-端口復用函數 52P-半關閉及shutdown函數 端口復用:
int opt = 1; // 設置端口復用。setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半關閉:
通信雙方中,只有一端關閉通信。 --- FIN_WAIT_2close(cfd);shutdown(int fd, int how); how: SHUT_RD 關讀端SHUT_WR 關寫端SHUT_RDWR 關讀寫shutdown在關閉多個文件描述符應用的文件時,采用全關閉方法。close,只關閉一個。
53P-多路IO轉接服務器設計思路
54P-select函數參數簡介 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:監聽的所有文件描述符中,最大文件描述符+1readfds: 讀 文件描述符監聽集合。 傳入、傳出參數writefds:寫 文件描述符監聽集合。 傳入、傳出參數 NULLexceptfds:異常 文件描述符監聽集合 傳入、傳出參數 NULLtimeout: > 0: 設置監聽超時時長。NULL: 阻塞監聽0: 非阻塞監聽,輪詢返回值:> 0: 所有監聽集合(3個)中, 滿足對應事件的總數。0: 沒有滿足監聽條件的文件描述符-1: errno
55P-中午復習
56P-select函數原型分析 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:監聽的所有文件描述符中,最大文件描述符+1readfds: 讀 文件描述符監聽集合。 傳入、傳出參數writefds:寫 文件描述符監聽集合。 傳入、傳出參數 NULLexceptfds:異常 文件描述符監聽集合 傳入、傳出參數 NULLtimeout: > 0: 設置監聽超時時長。NULL: 阻塞監聽0: 非阻塞監聽,輪詢返回值:> 0: 所有監聽集合(3個)中, 滿足對應事件的總數。0: 沒有滿足監聽條件的文件描述符-1: errno
57P-select相關函數參數分析 void FD_CLR(int fd, fd_set *set) 把某一個fd清除出去 int FD_ISSET(int fd, fd_set *set) 判定某個fd是否在位圖中 void FD_SET(int fd, fd_set *set) 把某一個fd添加到位圖 void FD_ZERO(fd_set *set) 位圖所有二進制位置零
select多路IO轉接:
原理: 借助內核, select 來監聽, 客戶端連接、數據通信事件。void FD_ZERO(fd_set *set); --- 清空一個文件描述符集合。fd_set rset;FD_ZERO(&rset);void FD_SET(int fd, fd_set *set); --- 將待監聽的文件描述符,添加到監聽集合中FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);void FD_CLR(int fd, fd_set *set); --- 將一個文件描述符從監聽集合中 移除。FD_CLR(4, &rset);int FD_ISSET(int fd, fd_set *set); --- 判斷一個文件描述符是否在監聽集合中。返回值: 在:1;不在:0;FD_ISSET(4, &rset);
58P-select實現多路IO轉接設計思路 思路分析:
int maxfd = 0;lfd = socket() ; 創建套接字maxfd = lfd;bind(); 綁定地址結構listen(); 設置監聽上限fd_set rset, allset; 創建r監聽集合FD_ZERO(&allset); 將r監聽集合清空FD_SET(lfd, &allset); 將 lfd 添加至讀集合中。while(1) {rset = allset; 保存監聽集合ret = select(lfd+1, &rset, NULL, NULL, NULL); 監聽文件描述符集合對應事件。if(ret > 0) { 有監聽的描述符滿足對應事件if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。cfd = accept(); 建立連接,返回用于通信的文件描述符maxfd = cfd;FD_SET(cfd, &allset); 添加到監聽通信描述符集合中。}for (i = lfd+1; i <= 最大文件描述符; i++){FD_ISSET(i, &rset) 有read、write事件read()小 -- 大write();} }
}
59P-select實現多路IO轉接-代碼review
代碼如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <ctype.h> #include “wrap.h” #define SERV_PORT 6666 int main(int argc, char *argv[]) { int i, j, n, nready;
int maxfd = 0;
int listenfd, connfd;
char buf[BUFSIZ]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
fd_set rset, allset; /* rset 讀事件文件描述符集合 allset用來暫存 */
maxfd = listenfd;
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 構造select監控文件描述符集 */
while (1) {
rset = allset; /* 每次循環時都從新設置select監控信號集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 說明有新的客戶端鏈接請求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不會阻塞 */
FD_SET(connfd, &allset); /* 向監控文件描述符集合allset添加新的文件描述符connfd */
if (maxfd < connfd)
maxfd = connfd;
if (0 == --nready) /* 只有listenfd有事件, 后續的 for 不需執行 */
continue;
}
for (i = listenfd+1; i <= maxfd; i++) { /* 檢測哪個clients 有數據就緒 */
if (FD_ISSET(i, &rset)) {
if ((n = Read(i, buf, sizeof(buf))) == 0) { /* 當client關閉鏈接時,服務器端也關閉對應鏈接 */
Close(i);
FD_CLR(i, &allset); /* 解除select對此文件描述符的監控 */
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(i, buf, n);
}
}
}
}
Close(listenfd);
return 0;
}
編譯運行,結果如下:
如圖,借助select也可以實現多線程
60P-select實現多路IO轉接-代碼實現 61P-select實現多路IO轉接-添加注釋
代碼太長了,直接看59話吧
62P-select優缺點
select優缺點:
缺點: 監聽上限受文件描述符限制。 最大 1024.檢測滿足條件的fd, 自己添加業務邏輯提高小。 提高了編碼難度。優點: 跨平臺。win、linux、macOS、Unix、類Unix、mips
select代碼里有個可以優化的地方,用數組存下文件描述符,這樣就不需要每次掃描一大堆無關文件描述符了
63P-添加一個自定義數組提高效率 這里就是改進之前代碼的問題,之前的代碼,如果最大fd是1023,每次確定有事件發生的fd時,就要掃描3-1023的所有文件描述符,這看起來很蠢。于是定義一個數組,把要監聽的文件描述符存下來,每次掃描這個數組就行了??雌饋砜茖W得多。
如圖,加個client數組,存要監聽的描述符。 代碼如下,挺長的
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <ctype.h> #include “wrap.h” #define SERV_PORT 6666 int main(int argc, char *argv[]) { int i, j, n, maxi;
int nready, client[FD_SETSIZE]; /* 自定義數組client, 防止遍歷1024個文件描述符 FD_SETSIZE默認為1024 */
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset; /* rset 讀事件文件描述符集合 allset用來暫存 */
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
maxfd = listenfd; /* 起初 listenfd 即為最大文件描述符 */
maxi = -1; /* 將來用作client[]的下標, 初始值指向0個元素之前下標位置 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 構造select監控文件描述符集 */
while (1) {
rset = allset; /* 每次循環時都重新設置select監控信號集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 說明有新的客戶端鏈接請求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不會阻塞 */
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { /* 找client[]中沒有使用的位置 */
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
if (i == FD_SETSIZE) { /* 達到select能監控的文件個數上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 向監控文件描述符集合allset添加新的文件描述符connfd */
if (connfd > maxfd)
maxfd = connfd; /* select第一個參數需要 */
if (i > maxi)
maxi = i; /* 保證maxi存的總是client[]最后一個元素下標 */
if (--nready == 0)
continue;
}
for (i = 0; i <= maxi; i++) { /* 檢測哪個clients 有數據就緒 */
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 當client關閉鏈接時,服務器端也關閉對應鏈接 */
Close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select對此文件描述符的監控 */
client[i] = -1;
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0)
break; /* 跳出for, 但還在while中 */
}
}
}
Close(listenfd);
return 0;
}
編譯運行和改進前沒啥區別,這里就不貼圖了 64P-總結
TCP狀態時序圖:
結合三次握手、四次揮手 理解記憶。1. 主動發起連接請求端: CLOSE -- 發送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 發送 ACK -- ESTABLISHED(數據通信態)2. 主動關閉連接請求端: ESTABLISHED(數據通信態) -- 發送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半關閉)-- 接收對端發送 FIN -- FIN_WAIT_2(半關閉)-- 回發ACK -- TIME_WAIT(只有主動關閉連接方,會經歷該狀態)-- 等 2MSL時長 -- CLOSE 3. 被動接收連接請求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 發送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(數據通信態)4. 被動關閉連接請求端: ESTABLISHED(數據通信態) -- 接收 FIN -- ESTABLISHED(數據通信態) -- 發送ACK -- CLOSE_WAIT (說明對端【主動關閉連接端】處于半關閉狀態) -- 發送FIN -- LAST_ACK -- 接收ACK -- CLOSE重點記憶: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)netstat -apn | grep 端口號
2MSL時長:
一定出現在【主動關閉連接請求端】。 --- 對應 TIME_WAIT 狀態。保證,最后一個 ACK 能成功被對端接收。(等待期間,對端沒收到我發的ACK,對端會再次發送FIN請求。)
端口復用:
int opt = 1; // 設置端口復用。setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半關閉:
通信雙方中,只有一端關閉通信。 --- FIN_WAIT_2close(cfd);shutdown(int fd, int how); how: SHUT_RD 關讀端SHUT_WR 關寫端SHUT_RDWR 關讀寫shutdown在關閉多個文件描述符應用的文件時,采用全關閉方法。close,只關閉一個。
select多路IO轉接:
原理: 借助內核, select 來監聽, 客戶端連接、數據通信事件。void FD_ZERO(fd_set *set); --- 清空一個文件描述符集合。fd_set rset;FD_ZERO(&rset);void FD_SET(int fd, fd_set *set); --- 將待監聽的文件描述符,添加到監聽集合中FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);void FD_CLR(int fd, fd_set *set); --- 將一個文件描述符從監聽集合中 移除。FD_CLR(4, &rset);int FD_ISSET(int fd, fd_set *set); --- 判斷一個文件描述符是否在監聽集合中。返回值: 在:1;不在:0;FD_ISSET(4, &rset);int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);nfds:監聽的所有文件描述符中,最大文件描述符+1readfds: 讀 文件描述符監聽集合。 傳入、傳出參數writefds:寫 文件描述符監聽集合。 傳入、傳出參數 NULLexceptfds:異常 文件描述符監聽集合 傳入、傳出參數 NULLtimeout: > 0: 設置監聽超時時長。NULL: 阻塞監聽0: 非阻塞監聽,輪詢返回值:> 0: 所有監聽集合(3個)中, 滿足對應事件的總數。0: 沒有滿足監聽條件的文件描述符-1: errno
思路分析:
int maxfd = 0;lfd = socket() ; 創建套接字maxfd = lfd;bind(); 綁定地址結構listen(); 設置監聽上限fd_set rset, allset; 創建r監聽集合FD_ZERO(&allset); 將r監聽集合清空FD_SET(lfd, &allset); 將 lfd 添加至讀集合中。while(1) {rset = allset; 保存監聽集合ret = select(lfd+1, &rset, NULL, NULL, NULL); 監聽文件描述符集合對應事件。if(ret > 0) { 有監聽的描述符滿足對應事件if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。cfd = accept(); 建立連接,返回用于通信的文件描述符maxfd = cfd;FD_SET(cfd, &allset); 添加到監聽通信描述符集合中。}for (i = lfd+1; i <= 最大文件描述符; i++){FD_ISSET(i, &rset) 有read、write事件read()小 -- 大write();} }
}
select優缺點:
缺點: 監聽上限受文件描述符限制。 最大 1024.檢測滿足條件的fd, 自己添加業務邏輯提高小。 提高了編碼難度。優點: 跨平臺。win、linux、macOS、Unix、類Unix、mips
65P-復習
66P-poll函數原型分析 poll是對select的改進,但是它是個半成品,相對select提升不大。最終版本是epoll,所以poll了解一下就完事兒,重點掌握epoll。
poll: int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:監聽的文件描述符【數組】struct pollfd {int fd: 待監聽的文件描述符short events: 待監聽的文件描述符對應的監聽事件取值:POLLIN、POLLOUT、POLLERRshort revnets: 傳入時, 給0。如果滿足對應事件的話, 返回 非0 --> POLLIN、POLLOUT、POLLERR}nfds: 監聽數組的,實際有效監聽個數。timeout: > 0: 超時時長。單位:毫秒。-1: 阻塞等待0: 不阻塞返回值:返回滿足對應監聽事件的文件描述符 總個數。優點:自帶數組結構。 可以將 監聽事件集合 和 返回事件集合 分離。拓展 監聽上限。 超出 1024限制。缺點:不能跨平臺。 Linux無法直接定位滿足監聽事件的文件描述符, 編碼難度較大。
67P-poll函數使用注意事項示例
68P-poll函數實現服務器 這個東西用得少,基本都用epoll,從講義上掛個代碼過來,看看視頻里思路就完事兒
/* server.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <poll.h> #include <errno.h> #include “wrap.h” #define MAXLINE 80 #define SERV_PORT 6666 #define OPEN_MAX 1024 int main(int argc, char *argv[]) { int i, j, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
client[0].fd = listenfd;
client[0].events = POLLRDNORM; /* listenfd監聽普通讀事件 */
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
maxi = 0; /* client[]數組有效元素中最大元素下標 */
for ( ; ; ) {
nready = poll(client, maxi+1, -1); /* 阻塞 */
if (client[0].revents & POLLRDNORM) { /* 有客戶端鏈接請求 */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd < 0) {
client[i].fd = connfd; /* 找到client[]中空閑的位置,存放accept返回的connfd */
break;
}
}
if (i == OPEN_MAX)
perr_exit("too many clients");
client[i].events = POLLRDNORM; /* 設置剛剛返回的connfd,監控讀事件 */
if (i > maxi)
maxi = i; /* 更新client[]中最大元素下標 */
if (--nready <= 0)
continue; /* 沒有更多就緒事件時,繼續回到poll阻塞 */
}
for (i = 1; i <= maxi; i++) { /* 檢測client[] */
if ((sockfd = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR)) {
if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) { /* 當收到 RST標志時 */
/* connection reset by client */
printf("client[%d] aborted connection\n", i);
Close(sockfd);
client[i].fd = -1;
} else {
perr_exit("read error");
}
} else if (n == 0) {
/* connection closed by client */
printf("client[%d] closed connection\n", i);
Close(sockfd);
client[i].fd = -1;
} else {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Writen(sockfd, buf, n);
}
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
return 0;
}
69P-poll總結 優點: 自帶數組結構。 可以將 監聽事件集合 和 返回事件集合 分離。
拓展 監聽上限。 超出 1024限制。
缺點: 不能跨平臺。 Linux
無法直接定位滿足監聽事件的文件描述符, 編碼難度較大。
70P-epoll函數實現的多路IO轉接
代碼如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <errno.h> #include <ctype.h> #include “wrap.h” #define MAXLINE 8192 #define SERV_PORT 8000 #define OPEN_MAX 5000 int main(int argc, char *argv[]) { int i, listenfd, connfd, sockfd;
int n, num = 0;
ssize_t nready, efd, res;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX]; //tep: epoll_ctl參數 ep[] : epoll_wait參數
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口復用
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
Listen(listenfd, 20);
efd = epoll_create(OPEN_MAX); //創建epoll模型, efd指向紅黑樹根節點
if (efd == -1)
perr_exit("epoll_create error");
tep.events = EPOLLIN;
tep.data.fd = listenfd; //指定lfd的監聽時間為"讀"
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); //將lfd及對應的結構體設置到樹上,efd可找到該樹
if (res == -1)
perr_exit("epoll_ctl error");
for ( ; ; ) {
/*epoll為server阻塞監聽事件, ep為struct epoll_event類型數組, OPEN_MAX為數組容量, -1表永久阻塞*/
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
if (nready == -1)
perr_exit("epoll_wait error");
for (i = 0; i < nready; i++) {
if (!(ep[i].events & EPOLLIN)) //如果不是"讀"事件, 繼續循環
continue;
if (ep[i].data.fd == listenfd) { //判斷滿足事件的fd是不是lfd
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); //接受鏈接
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
printf("cfd %d---client %d\n", connfd, ++num);
tep.events = EPOLLIN; tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); //加入紅黑樹
if (res == -1)
perr_exit("epoll_ctl error");
} else { //不是lfd,
sockfd = ep[i].data.fd;
n = Read(sockfd, buf, MAXLINE);
if (n == 0) { //讀到0,說明客戶端關閉鏈接
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); //將該文件描述符從紅黑樹摘除
if (res == -1)
perr_exit("epoll_ctl error");
Close(sockfd); //關閉與該客戶端的鏈接
printf("client[%d] closed connection\n", sockfd);
} else if (n < 0) { //出錯
perror("read n < 0 error: ");
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); //摘除節點
Close(sockfd);
} else { //實際讀到了字節數
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]); //轉大寫,寫回給客戶端
Write(STDOUT_FILENO, buf, n);
Writen(sockfd, buf, n);
}
}
}
}
Close(listenfd);
Close(efd);
return 0;
}
71P-突破1024文件描述符設置
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 當前計算機所能打開的最大文件個數。 受硬件影響。ulimit -a ——> 當前用戶下的進程,默認打開文件描述符個數。 缺省為 1024修改:打開 sudo vi /etc/security/limits.conf, 寫入:* soft nofile 65536 --> 設置默認值, 可以直接借助命令修改。 【注銷用戶,使其生效】* hard nofile 100000 --> 命令修改上限。
cat /proc/sys/fs/file-max 查看最大文件描述符上限
ulimit -a
sudo vi /etc/security/limits.conf 修改上限
修改之后,注銷用戶重新登錄,查看文件描述符上限:
如圖,已經修改成功了。
如果使用ulimit -n 來修改,會受到之前設置的hard的限制:
用ulimit -n設置之后,往下調可以,往上調需要注銷用戶再登錄。
72P-epoll_create和epoll_ctl
epoll: int epoll_create(int size); 創建一棵監聽紅黑樹
size:創建的紅黑樹的監聽節點數量。(僅供內核參考。)返回值:指向新創建的紅黑樹的根節點的 fd。 失敗: -1 errnoint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作監聽紅黑樹epfd:epoll_create 函數的返回值。 epfdop:對該監聽紅黑數所做的操作。EPOLL_CTL_ADD 添加fd到 監聽紅黑樹EPOLL_CTL_MOD 修改fd在 監聽紅黑樹上的監聽事件。EPOLL_CTL_DEL 將一個fd 從監聽紅黑樹上摘下(取消監聽)fd:待監聽的fdevent: 本質 struct epoll_event 結構體 地址成員 events:EPOLLIN / EPOLLOUT / EPOLLERR成員 data: 聯合體(共用體):int fd; 對應監聽事件的 fdvoid *ptr; uint32_t u32;uint64_t u64; 返回值:成功 0; 失敗: -1 errno
73P-epoll_wait函數 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞監聽。
epfd:epoll_create 函數的返回值。 epfdevents:傳出參數,【數組】, 滿足監聽條件的 那些 fd 結構體。maxevents:數組 元素的總個數。 1024struct epoll_event evnets[1024]timeout:-1: 阻塞0: 不阻塞>0: 超時時間 (毫秒)返回值:> 0: 滿足監聽的 總個數。 可以用作循環上限。0: 沒有fd滿足監聽事件-1:失敗。 errno
74P-中午復習 epoll實現多路IO轉接思路:
lfd = socket(); 監聽連接事件lfd bind(); listen();
int epfd = epoll_create(1024); epfd, 監聽紅黑樹的樹根。
struct epoll_event tep, ep[1024]; tep, 用來設置單個fd屬性, ep 是 epoll_wait() 傳出的滿足監聽事件的數組。
tep.events = EPOLLIN; 初始化 lfd的監聽屬性。 tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 將 lfd 添加到監聽紅黑樹上。
while (1) {
ret = epoll_wait(epfd, ep,1024, -1); 實施監聽for (i = 0; i < ret; i++) {if (ep[i].data.fd == lfd) { // lfd 滿足讀事件,有新的客戶端發起連接請求cfd = Accept();tep.events = EPOLLIN; 初始化 cfd的監聽屬性。tep.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);} else { cfd 們 滿足讀事件, 有客戶端寫數據來。n = read(ep[i].data.fd, buf, sizeof(buf));if ( n == 0) {close(ep[i].data.fd);epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL); // 將關閉的cfd,從監聽樹上摘下。} else if (n > 0) {小--大write(ep[i].data.fd, buf, n);}}
}
75P-ET和LT模式 epoll是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量并發連接中只有少量活躍的情況下的系統CPU利用率,因為它會復用文件描述符集合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新準備要被偵聽的文件描述符集合,另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。
EPOLL事件有兩種模型: Edge Triggered (ET) 邊緣觸發只有數據到來才觸發,不管緩存區中是否還有數據。 Level Triggered (LT) 水平觸發只要有數據都會觸發。
視頻中epoll測試代碼如下,用一個子進程來寫內容,用ET和LT模式來讀取,結果很能說明問題:
#include <stdio.h> #include <stdlib.h> #include <sys/epoll.h> #include <errno.h> #include <unistd.h> #define MAXLINE 10 int main(int argc, char *argv[]) { int efd, i;
int pfd[2];
pid_t pid;
char buf[MAXLINE], ch = 'a';
pipe(pfd);
pid = fork();
if (pid == 0) { //子 寫
close(pfd[0]);
while (1) {
//aaaa\n
for (i = 0; i < MAXLINE/2; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//bbbb\n
for (; i < MAXLINE; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//aaaa\nbbbb\n
write(pfd[1], buf, sizeof(buf));
sleep(5);
}
close(pfd[1]);
} else if (pid > 0) { //父 讀
struct epoll_event event;
struct epoll_event resevent[10]; //epoll_wait就緒返回event
int res, len;
close(pfd[1]);
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; // ET 邊沿觸發
// event.events = EPOLLIN; // LT 水平觸發 (默認)
event.data.fd = pfd[0];
epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == pfd[0]) {
len = read(pfd[0], buf, MAXLINE/2);
write(STDOUT_FILENO, buf, len);
}
}
close(pfd[0]);
close(efd);
} else {
perror("fork");
exit(-1);
}
return 0;
}
簡單理解就是,水平觸發就是有數據就觸發,邊沿觸發是有新數據進來才觸發。學電子的就比較清楚,觸發器就有這個分類。
76P-網絡中ET和LT模式 直接看代碼,server代碼如下:
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #define MAXLINE 10 #define SERV_PORT 9000 int main(void) { struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int efd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; /* ET 邊沿觸發 */
//event.events = EPOLLIN; /* 默認 LT 水平觸發 */
printf("Accepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == connfd) {
len = read(connfd, buf, MAXLINE/2); //readn(500)
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
client代碼如下:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #define MAXLINE 10 #define SERV_PORT 9000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, i;
char ch = 'a';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (1) {
//aaaa\n
for (i = 0; i < MAXLINE/2; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//bbbb\n
for (; i < MAXLINE; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//aaaa\nbbbb\n
write(sockfd, buf, sizeof(buf));
sleep(5);
}
close(sockfd);
return 0;
}
server邊沿觸發,編譯運行,結果如下:
運行后,每過5秒鐘服務器才輸出一組字符,這是就是邊沿觸發的效果。
更改服務器為水平觸發模式,運行程序,如下:
運行后,每5秒輸出兩組字符串,這是因為只寫入了兩組,這個模式的服務器,緩沖區有多少讀多少。
ET模式:
邊沿觸發:緩沖區剩余未讀盡的數據不會導致 epoll_wait 返回。 新的事件滿足,才會觸發。struct epoll_event event;event.events = EPOLLIN | EPOLLET;
LT模式:水平觸發 -- 默認采用模式。緩沖區剩余未讀盡的數據會導致 epoll_wait 返回。
77P-epoll的ET非阻塞模式 readn調用的阻塞,比如設定讀500個字符,但是只讀到498,完事兒阻塞了,等另剩下的2個字符,然而在server代碼里,一旦read變為readn阻塞了,它就不會被喚醒了,因為epoll_wait因為readn的阻塞不會循環執行,讀不到新數據。有點死鎖的意思,差倆字符所以阻塞,因為阻塞,讀不到新字符。
LT(level triggered):LT是缺省的工作方式,并且同時支持block和no-block socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。 ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,并且不會再為那個文件描述符發送更多的就緒通知。請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once).
用fcntl設置阻塞
非阻塞epoll的服務器代碼如下:
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #define MAXLINE 10 #define SERV_PORT 8000 int main(void) { struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int efd, flag;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
///
struct epoll_event event;
struct epoll_event res_event[10];
int res, len;
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; /* ET 邊沿觸發,默認是水平觸發 */
//event.events = EPOLLIN;
printf("Accepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
flag = fcntl(connfd, F_GETFL); /* 修改connfd為非阻塞讀 */
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event); //將connfd加入監聽紅黑樹
while (1) {
printf("epoll_wait begin\n");
res = epoll_wait(efd, res_event, 10, -1); //最多10個, 阻塞監聽
printf("epoll_wait end res %d\n", res);
if (res_event[0].data.fd == connfd) {
while ((len = read(connfd, buf, MAXLINE/2)) >0 ) //非阻塞讀, 輪詢
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
其實就是多了這幾行:
結論: epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 — 忙輪詢。
struct epoll_event event;event.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event); int flg = fcntl(cfd, F_GETFL); flg |= O_NONBLOCK;fcntl(cfd, F_SETFL, flg);優點:高效。突破1024文件描述符。缺點:不能跨平臺。 Linux。
后面使用epoll就用這種非阻塞的
78P-epoll優缺點總結 優點:
高效。突破1024文件描述符。
缺點: 不能跨平臺。 Linux。
79P-補充對比ET和LT 這里重要的就一點,當使用非阻塞讀時,讀取數據需要輪詢。 比如使用readn的時候,數據沒讀夠,因為非阻塞,跑了,想讀剩下的,就得輪詢。
80P-epoll反應堆模型總述 epoll 反應堆模型:
epoll ET模式 + 非阻塞、輪詢 + void *ptr。原來: socket、bind、listen -- epoll_create 創建監聽 紅黑樹 -- 返回 epfd -- epoll_ctl() 向樹上添加一個監聽fd -- while(1)---- epoll_wait 監聽 -- 對應監聽fd有事件產生 -- 返回 監聽滿足數組。 -- 判斷返回數組元素 -- lfd滿足 -- Accept -- cfd 滿足 -- read() --- 小->大 -- write回去。
反應堆:不但要監聽 cfd 的讀事件、還要監聽cfd的寫事件。
socket、bind、listen -- epoll_create 創建監聽 紅黑樹 -- 返回 epfd -- epoll_ctl() 向樹上添加一個監聽fd -- while(1)---- epoll_wait 監聽 -- 對應監聽fd有事件產生 -- 返回 監聽滿足數組。 -- 判斷返回數組元素 -- lfd滿足 -- Accept -- cfd 滿足 -- read() --- 小->大 -- cfd從監聽紅黑樹上摘下 -- EPOLLOUT -- 回調函數 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到紅黑上監聽寫事件-- 等待 epoll_wait 返回 -- 說明 cfd 可寫 -- write回去 -- cfd從監聽紅黑樹上摘下 -- EPOLLIN -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到紅黑上監聽讀事件 -- epoll_wait 監聽
反應堆的理解:加入IO轉接之后,有了事件,server才去處理,這里反應堆也是這樣,由于網絡環境復雜,服務器處理數據之后,可能并不能直接寫回去,比如遇到網絡繁忙或者對方緩沖區已經滿了這種情況,就不能直接寫回給客戶端。反應堆就是在處理數據之后,監聽寫事件,能寫會客戶端了,才去做寫回操作。寫回之后,再改為監聽讀事件。如此循環。
81P-epoll反應堆main邏輯 直接上代碼,這就略微有點長了:
/* *epoll基于非阻塞I/O事件驅動 */ #include <stdio.h> #include <sys/socket.h> #include <sys/epoll.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <stdlib.h> #include <time.h> #define MAX_EVENTS 1024 //監聽上限數 #define BUFLEN 4096 #define SERV_PORT 8080 void recvdata(int fd, int events, void *arg); void senddata(int fd, int events, void *arg); /* 描述就緒文件描述符相關信息 */ struct myevent_s { int fd; //要監聽的文件描述符
int events; //對應的監聽事件
void *arg; //泛型參數
void (*call_back)(int fd, int events, void *arg); //回調函數
int status; //是否在監聽:1->在紅黑樹上(監聽), 0->不在(不監聽)
char buf[BUFLEN];
int len;
long last_active; //記錄每次加入紅黑樹 g_efd 的時間值
}; int g_efd; //全局變量, 保存epoll_create返回的文件描述符 struct myevent_s g_events[MAX_EVENTS+1]; //自定義結構體類型數組. +1–>listen fd /將結構體 myevent_s 成員變量 初始化/ void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg) { ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
ev->last_active = time(NULL); //調用eventset函數的時間
return;
} /* 向 epoll監聽的紅黑樹 添加一個 文件描述符 */ //eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); void eventadd(int efd, int events, struct myevent_s *ev) { struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if (ev->status == 0) { //已經在紅黑樹 g_efd 里
op = EPOLL_CTL_ADD; //將其加入紅黑樹 g_efd, 并將status置1
ev->status = 1;
}
if (epoll_ctl(efd, op, ev->fd, &epv) < 0) //實際添加/修改
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);
return ;
} /* 從epoll 監聽的 紅黑樹中刪除一個 文件描述符*/ void eventdel(int efd, struct myevent_s *ev) { struct epoll_event epv = {0, {0}};
if (ev->status != 1) //不在紅黑樹上
return ;
//epv.data.ptr = ev;
epv.data.ptr = NULL;
ev->status = 0; //修改狀態
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); //從紅黑樹 efd 上將 ev->fd 摘除
return ;
} /* 當有文件描述符就緒, epoll返回, 調用該函數 與客戶端建立鏈接 */ void acceptconn(int lfd, int events, void *arg) { struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
/* 暫時不做出錯處理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return ;
}
do {
for (i = 0; i < MAX_EVENTS; i++) //從全局數組g_events中找一個空閑元素
if (g_events[i].status == 0) //類似于select中找值為-1的元素
break; //跳出 for
if (i == MAX_EVENTS) {
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //跳出do while(0) 不執行后續代碼
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) { //將cfd也設置為非阻塞
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
/* 給cfd設置一個 myevent_s 結構體, 回調函數 設置為 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]);
eventadd(g_efd, EPOLLIN, &g_events[i]); //將cfd添加到紅黑樹g_efd中,監聽讀事件
} while(0);
printf("new connect [%s:%d][time:%ld], pos[%d]\n",
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return ;
} void recvdata(int fd, int events, void *arg) { struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0); //讀文件描述符, 數據存入myevent_s成員buf中
eventdel(g_efd, ev); //將該節點從紅黑樹上摘除
if (len > 0) {
ev->len = len;
ev->buf[len] = '\0'; //手動添加字符串結束標記
printf("C[%d]:%s\n", fd, ev->buf);
eventset(ev, fd, senddata, ev); //設置該 fd 對應的回調函數為 senddata
eventadd(g_efd, EPOLLOUT, ev); //將fd加入紅黑樹g_efd中,監聽其寫事件
} else if (len == 0) {
close(ev->fd);
/* ev-g_events 地址相減得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
} else {
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
} void senddata(int fd, int events, void *arg) { struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0); //直接將數據 回寫給客戶端。未作處理
eventdel(g_efd, ev); //從紅黑樹g_efd中移除
if (len > 0) {
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev); //將該fd的 回調函數改為 recvdata
eventadd(g_efd, EPOLLIN, ev); //從新添加到紅黑樹上, 設為監聽讀事件
} else {
close(ev->fd); //關閉鏈接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
} /*創建 socket, 初始化lfd */ void initlistensocket(int efd, short port) { struct sockaddr_in sin;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //將socket設為非阻塞
memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
return ;
} int main(int argc, char *argv[]) { unsigned short port = SERV_PORT;
if (argc == 2)
port = atoi(argv[1]); //使用用戶指定端口.如未指定,用默認端口
g_efd = epoll_create(MAX_EVENTS+1); //創建紅黑樹,返回給全局 g_efd
if (g_efd <= 0)
printf("create efd in %s err %s\n", __func__, strerror(errno));
initlistensocket(g_efd, port); //初始化監聽socket
struct epoll_event events[MAX_EVENTS+1]; //保存已經滿足就緒事件的文件描述符數組
printf("server running:port[%d]\n", port);
int checkpos = 0, i;
while (1) {
/* 超時驗證,每次測試100個鏈接,不測試listenfd 當客戶端60秒內沒有和服務器通信,則關閉此客戶端鏈接 */
long now = time(NULL); //當前時間
for (i = 0; i < 100; i++, checkpos++) { //一次循環檢測100個。 使用checkpos控制檢測對象
if (checkpos == MAX_EVENTS)
checkpos = 0;
if (g_events[checkpos].status != 1) //不在紅黑樹 g_efd 上
continue;
long duration = now - g_events[checkpos].last_active; //客戶端不活躍的世間
if (duration >= 60) {
close(g_events[checkpos].fd); //關閉與該客戶端鏈接
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]); //將該客戶端 從紅黑樹 g_efd移除
}
}
/*監聽紅黑樹g_efd, 將滿足的事件的文件描述符加至events數組中, 1秒沒有事件滿足, 返回 0*/
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0) {
printf("epoll_wait error, exit\n");
break;
}
for (i = 0; i < nfd; i++) {
/*使用自定義結構體myevent_s類型指針, 接收 聯合體data的void *ptr成員*/
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { //讀就緒事件
ev->call_back(ev->fd, events[i].events, ev->arg);
//lfd EPOLLIN
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { //寫就緒事件
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
/* 退出前釋放所有資源 */
return 0;
}
main邏輯:創建套接字—》初始化連接—》超時驗證—》監聽—》處理讀事件和寫事件
82P-epoll反應堆-給lfd和cfd指定回調函數 eventset函數指定了不同事件對應的回調函數,所以雖然讀寫事件都用的call_back來回調,但實際上調用的是不同的函數。
83P-epoll反應堆initlistensocket小總結 eventset函數:
設置回調函數。 lfd --》 acceptconn()cfd --> recvdata();cfd --> senddata();
eventadd函數:
將一個fd, 添加到 監聽紅黑樹。 設置監聽 read事件,還是監聽寫事件。
84P-epoll反應堆wait被觸發后read和write回調及監聽 網絡編程中: read — recv()
write --- send();
85P-epoll反應堆-超時時間 用一個last_active存儲上次活躍時間,完事兒用當前時間和上次活躍時間來計算不活躍時間長度,不活躍時間超過一定閾值,就踢掉這個客戶端。
86-總結 多路IO轉接:
select:
poll: int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:監聽的文件描述符【數組】struct pollfd {int fd: 待監聽的文件描述符short events: 待監聽的文件描述符對應的監聽事件取值:POLLIN、POLLOUT、POLLERRshort revnets: 傳入時, 給0。如果滿足對應事件的話, 返回 非0 --> POLLIN、POLLOUT、POLLERR}nfds: 監聽數組的,實際有效監聽個數。timeout: > 0: 超時時長。單位:毫秒。-1: 阻塞等待0: 不阻塞返回值:返回滿足對應監聽事件的文件描述符 總個數。優點:自帶數組結構。 可以將 監聽事件集合 和 返回事件集合 分離。拓展 監聽上限。 超出 1024限制。缺點:不能跨平臺。 Linux無法直接定位滿足監聽事件的文件描述符, 編碼難度較大。
read 函數返回值:
> 0: 實際讀到的字節數=0: socket中,表示對端關閉。close()-1: 如果 errno == EINTR 被異常終端。 需要重啟。如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式讀數據,但是沒有數據。 需要,再次讀。如果 errno == ECONNRESET 說明連接被 重置。 需要 close(),移除監聽隊列。錯誤。
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 當前計算機所能打開的最大文件個數。 受硬件影響。ulimit -a ——> 當前用戶下的進程,默認打開文件描述符個數。 缺省為 1024修改:打開 sudo vi /etc/security/limits.conf, 寫入:* soft nofile 65536 --> 設置默認值, 可以直接借助命令修改。 【注銷用戶,使其生效】* hard nofile 100000 --> 命令修改上限。
epoll: int epoll_create(int size); 創建一棵監聽紅黑樹
size:創建的紅黑樹的監聽節點數量。(僅供內核參考。)返回值:指向新創建的紅黑樹的根節點的 fd。 失敗: -1 errnoint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作監聽紅黑樹epfd:epoll_create 函數的返回值。 epfdop:對該監聽紅黑數所做的操作。EPOLL_CTL_ADD 添加fd到 監聽紅黑樹EPOLL_CTL_MOD 修改fd在 監聽紅黑樹上的監聽事件。EPOLL_CTL_DEL 將一個fd 從監聽紅黑樹上摘下(取消監聽)fd:待監聽的fdevent: 本質 struct epoll_event 結構體 地址成員 events:EPOLLIN / EPOLLOUT / EPOLLERR成員 data: 聯合體(共用體):int fd; 對應監聽事件的 fdvoid *ptr; uint32_t u32;uint64_t u64; 返回值:成功 0; 失敗: -1 errnoint epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞監聽。epfd:epoll_create 函數的返回值。 epfdevents:傳出參數,【數組】, 滿足監聽條件的 哪些 fd 結構體。maxevents:數組 元素的總個數。 1024struct epoll_event evnets[1024]timeout:-1: 阻塞0: 不阻塞>0: 超時時間 (毫秒)返回值:> 0: 滿足監聽的 總個數。 可以用作循環上限。0: 沒有fd滿足監聽事件-1:失敗。 errno
epoll實現多路IO轉接思路:
lfd = socket(); 監聽連接事件lfd bind(); listen();
int epfd = epoll_create(1024); epfd, 監聽紅黑樹的樹根。
struct epoll_event tep, ep[1024]; tep, 用來設置單個fd屬性, ep 是 epoll_wait() 傳出的滿足監聽事件的數組。
tep.events = EPOLLIN; 初始化 lfd的監聽屬性。 tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 將 lfd 添加到監聽紅黑樹上。
while (1) {
ret = epoll_wait(epfd, ep,1024, -1); 實施監聽for (i = 0; i < ret; i++) {if (ep[i].data.fd == lfd) { // lfd 滿足讀事件,有新的客戶端發起連接請求cfd = Accept();tep.events = EPOLLIN; 初始化 cfd的監聽屬性。tep.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);} else { cfd 們 滿足讀事件, 有客戶端寫數據來。n = read(ep[i].data.fd, buf, sizeof(buf));if ( n == 0) {close(ep[i].data.fd);epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL); // 將關閉的cfd,從監聽樹上摘下。} else if (n > 0) {小--大write(ep[i].data.fd, buf, n);}}
}
}
epoll 事件模型:
ET模式:邊沿觸發:緩沖區剩余未讀盡的數據不會導致 epoll_wait 返回。 新的事件滿足,才會觸發。struct epoll_event event;event.events = EPOLLIN | EPOLLET;
LT模式:水平觸發 -- 默認采用模式。緩沖區剩余未讀盡的數據會導致 epoll_wait 返回。結論:epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙輪詢。struct epoll_event event;event.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event); int flg = fcntl(cfd, F_GETFL); flg |= O_NONBLOCK;fcntl(cfd, F_SETFL, flg);優點:高效。突破1024文件描述符。缺點:不能跨平臺。 Linux。
epoll 反應堆模型:
epoll ET模式 + 非阻塞、輪詢 + void *ptr。原來: socket、bind、listen -- epoll_create 創建監聽 紅黑樹 -- 返回 epfd -- epoll_ctl() 向樹上添加一個監聽fd -- while(1)---- epoll_wait 監聽 -- 對應監聽fd有事件產生 -- 返回 監聽滿足數組。 -- 判斷返回數組元素 -- lfd滿足 -- Accept -- cfd 滿足 -- read() --- 小->大 -- write回去。反應堆:不但要監聽 cfd 的讀事件、還要監聽cfd的寫事件。socket、bind、listen -- epoll_create 創建監聽 紅黑樹 -- 返回 epfd -- epoll_ctl() 向樹上添加一個監聽fd -- while(1)---- epoll_wait 監聽 -- 對應監聽fd有事件產生 -- 返回 監聽滿足數組。 -- 判斷返回數組元素 -- lfd滿足 -- Accept -- cfd 滿足 -- read() --- 小->大 -- cfd從監聽紅黑樹上摘下 -- EPOLLOUT -- 回調函數 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到紅黑上監聽寫事件-- 等待 epoll_wait 返回 -- 說明 cfd 可寫 -- write回去 -- cfd從監聽紅黑樹上摘下 -- EPOLLIN -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到紅黑上監聽讀事件 -- epoll_wait 監聽eventset函數:設置回調函數。 lfd --》 acceptconn()cfd --> recvdata();cfd --> senddata();
eventadd函數:將一個fd, 添加到 監聽紅黑樹。 設置監聽 read事件,還是監聽寫事件。網絡編程中: read --- recv()write --- send();
87P-復習
88P-補充說明epoll的man手冊 89P-epoll反應堆再說明
90P-ctags使用
91P線程池模型原理分析
struct threadpool_t {
pthread_mutex_t lock; /* 用于鎖住本結構體 */
pthread_mutex_t thread_counter; /* 記錄忙狀態線程個數de瑣 -- busy_thr_num */pthread_cond_t queue_not_full; /* 當任務隊列滿時,添加任務的線程阻塞,等待此條件變量 */
pthread_cond_t queue_not_empty; /* 任務隊列里不為空時,通知等待任務的線程 */pthread_t *threads; /* 存放線程池中每個線程的tid。數組 */
pthread_t adjust_tid; /* 存管理線程tid */
threadpool_task_t *task_queue; /* 任務隊列(數組首地址) */int min_thr_num; /* 線程池最小線程數 */
int max_thr_num; /* 線程池最大線程數 */
int live_thr_num; /* 當前存活線程個數 */
int busy_thr_num; /* 忙狀態線程個數 */
int wait_exit_thr_num; /* 要銷毀的線程個數 */int queue_front; /* task_queue隊頭下標 */
int queue_rear; /* task_queue隊尾下標 */
int queue_size; /* task_queue隊中實際任務數 */
int queue_max_size; /* task_queue隊列可容納任務數上限 */int shutdown; /* 標志位,線程池使用狀態,true或false */
};
typedef struct {
void *(*function)(void *); /* 函數指針,回調函數 */
void arg; / 上面函數的參數 */
92P-線程池描述結構體 struct threadpool_t {
pthread_mutex_t lock; /* 用于鎖住本結構體 */
pthread_mutex_t thread_counter; /* 記錄忙狀態線程個數de瑣 -- busy_thr_num */pthread_cond_t queue_not_full; /* 當任務隊列滿時,添加任務的線程阻塞,等待此條件變量 */
pthread_cond_t queue_not_empty; /* 任務隊列里不為空時,通知等待任務的線程 */pthread_t *threads; /* 存放線程池中每個線程的tid。數組 */
pthread_t adjust_tid; /* 存管理線程tid */
threadpool_task_t *task_queue; /* 任務隊列(數組首地址) */int min_thr_num; /* 線程池最小線程數 */
int max_thr_num; /* 線程池最大線程數 */
int live_thr_num; /* 當前存活線程個數 */
int busy_thr_num; /* 忙狀態線程個數 */
int wait_exit_thr_num; /* 要銷毀的線程個數 */int queue_front; /* task_queue隊頭下標 */
int queue_rear; /* task_queue隊尾下標 */
int queue_size; /* task_queue隊中實際任務數 */
int queue_max_size; /* task_queue隊列可容納任務數上限 */int shutdown; /* 標志位,線程池使用狀態,true或false */
};
93P-線程池main架構
main();
創建線程池。向線程池中添加任務。 借助回調處理任務。銷毀線程池。
94P-線程池-pthreadpool_create 2. pthreadpool_create();
創建線程池結構體 指針。初始化線程池結構體 { N 個成員變量 }創建 N 個任務線程。創建 1 個管理者線程。失敗時,銷毀開辟的所有空間。(釋放)
95P-子線程回調函數 3. threadpool_thread()
進入子線程回調函數。接收參數 void *arg --》 pool 結構體加鎖 --》lock --》 整個結構體鎖判斷條件變量 --》 wait -------------------170
96P-管理者線程 4. adjust_thread()
循環 10 s 執行一次。進入管理者線程回調函數接收參數 void *arg --》 pool 結構體加鎖 --》lock --》 整個結構體鎖獲取管理線程池要用的到 變量。 task_num, live_num, busy_num根據既定算法,使用上述3變量,判斷是否應該 創建、銷毀線程池中 指定步長的線程。
97P-threadpool_add函數 5. threadpool_add ()
總功能:模擬產生任務。 num[20]設置回調函數, 處理任務。 sleep(1) 代表處理完成。內部實現:加鎖初始化 任務隊列結構體成員。 回調函數 function, arg利用環形隊列機制,實現添加任務。 借助隊尾指針挪移 % 實現。喚醒阻塞在 條件變量上的線程。解鎖
98P-條件滿足,子線程wait被喚醒后處理任務 6. 從 3. 中的wait之后繼續執行,處理任務。
加鎖獲取 任務處理回調函數,及參數利用環形隊列機制,實現處理任務。 借助隊頭指針挪移 % 實現。喚醒阻塞在 條件變量 上的 server。解鎖加鎖 改忙線程數++解鎖執行處理任務的線程加鎖 改忙線程數——解鎖
99P-線程池擴容和銷毀 7. 創建 銷毀線程
管理者線程根據 task_num, live_num, busy_num 根據既定算法,使用上述3變量,判斷是否應該 創建、銷毀線程池中 指定步長的線程。如果滿足 創建條件pthread_create(); 回調 任務線程函數。 live_num++如果滿足 銷毀條件wait_exit_thr_num = 10; signal 給 阻塞在條件變量上的線程 發送 假條件滿足信號 跳轉至 --170 wait阻塞線程會被 假信號 喚醒。判斷: wait_exit_thr_num > 0 pthread_exit();
100P-TCP和UDP通信優缺點 TCP通信和UDP通信各自的優缺點:
TCP: 面向連接的,可靠數據包傳輸。對于不穩定的網絡層,采取完全彌補的通信方式。 丟包重傳。優點:穩定。 數據流量穩定、速度穩定、順序缺點:傳輸速度慢。相率低。開銷大。使用場景:數據的完整型要求較高,不追求效率。大數據傳輸、文件傳輸。UDP: 無連接的,不可靠的數據報傳遞。對于不穩定的網絡層,采取完全不彌補的通信方式。 默認還原網絡狀況優點:傳輸速度塊。相率高。開銷小。缺點:不穩定。數據流量。速度。順序。使用場景:對時效性要求較高場合。穩定性其次。游戲、視頻會議、視頻電話。 騰訊、華為、阿里 --- 應用層數據校驗協議,彌補udp的不足。
101P-UDP通信server和client流程 UDP實現的 C/S 模型:
recv()/send() 只能用于 TCP 通信。 替代 read、writeaccpet(); ---- Connect(); ---被舍棄server:lfd = socket(AF_INET, STREAM, 0); SOCK_DGRAM --- 報式協議。bind();listen(); --- 可有可無while(1){read(cfd, buf, sizeof) --- 被替換 --- recvfrom() --- 涵蓋accept傳出地址結構。
小-- 大
write();--- 被替換 --- sendto()---- connect}close();
client:
connfd = socket(AF_INET, SOCK_DGRAM, 0);sendto(‘服務器的地址結構’, 地址結構大小)recvfrom()寫到屏幕close();
102P-recvfrom和sendto函數 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sockfd: 套接字buf:緩沖區地址len:緩沖區大小flags: 0src_addr:(struct sockaddr *)&addr 傳出。 對端地址結構addrlen:傳入傳出。返回值: 成功接收數據字節數。 失敗:-1 errn。 0: 對端關閉。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd: 套接字buf:存儲數據的緩沖區len:數據長度flags: 0src_addr:(struct sockaddr *)&addr 傳入。 目標地址結構addrlen:地址結構長度。返回值:成功寫出數據字節數。 失敗 -1, errno
103P-UDP實現的并發服務器和客戶端 直接上代碼,啃,啃就完事兒,這是服務器代碼
#include <string.h> #include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main(void) { struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
int sockfd;
char buf[BUFSIZ];
char str[INET_ADDRSTRLEN];
int i, n;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("Accepting connections ...\n");
while (1) {
clie_addr_len = sizeof(clie_addr);
n = recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)&clie_addr, &clie_addr_len);
if (n == -1)
perror("recvfrom error");
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr));
if (n == -1)
perror("sendto error");
}
close(sockfd);
return 0;
}
下面是客戶端代碼:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr;
int sockfd, n;
char buf[BUFSIZ];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, BUFSIZ, stdin) != NULL) {
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1)
perror("sendto error");
n = recvfrom(sockfd, buf, BUFSIZ, 0, NULL, 0); //NULL:不關心對端信息
if (n == -1)
perror("recvfrom error");
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}
104P-借助TCP的CS模型,改寫UDP的CS模型 看懂前面的,問題就不大了。可以再看一下視頻復習復習
105P-本地套接字和網絡套接字比較
本地套接字:
IPC: pipe、fifo、mmap、信號、本地套(domain)--- CS模型對比網絡編程 TCP C/S模型, 注意以下幾點:1. int socket(int domain, int type, int protocol); 參數 domain:AF_INET --> AF_UNIX/AF_LOCAL type: SOCK_STREAM/SOCK_DGRAM 都可以。
2. 地址結構: sockaddr_in --> sockaddr_unstruct sockaddr_in srv_addr; --> struct sockaddr_un srv_adrr;srv_addr.sin_family = AF_INET; --> srv_addr.sun_family = AF_UNIX;
· srv_addr.sin_port = htons(8888); strcpy(srv_addr.sun_path, “srv.socket”)
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); len = offsetof(struct sockaddr_un, sun_path) + strlen("srv.socket");bind(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); --> bind(fd, (struct sockaddr *)&srv_addr, len); 3. bind()函數調用成功,會創建一個 socket。因此為保證bind成功,通常我們在 bind之前, 可以使用 unlink("srv.socket");4. 客戶端不能依賴 “隱式綁定”。并且應該在通信建立過程中,創建且初始化2個地址結構:1) client_addr --> bind()2) server_addr --> connect();
106P-本地套接字通信 服務器代碼:
#include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <strings.h> #include <string.h> #include <ctype.h> #include <arpa/inet.h> #include <sys/un.h> #include <stddef.h> #include “wrap.h” #define SERV_ADDR “serv.socket” int main(void) { int lfd, cfd, len, size, i;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
lfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path, SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* servaddr total len */
unlink(SERV_ADDR); /* 確保bind之前serv.sock文件不存在,bind會創建該文件 */
Bind(lfd, (struct sockaddr *)&servaddr, len); /* 參3不能是sizeof(servaddr) */
Listen(lfd, 20);
printf("Accept ...\n");
while (1) {
len = sizeof(cliaddr); //AF_UNIX大小+108B
cfd = Accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&len);
len -= offsetof(struct sockaddr_un, sun_path); /* 得到文件名的長度 */
cliaddr.sun_path[len] = '\0'; /* 確保打印時,沒有亂碼出現 */
printf("client bind filename %s\n", cliaddr.sun_path);
while ((size = read(cfd, buf, sizeof(buf))) > 0) {
for (i = 0; i < size; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, size);
}
close(cfd);
}
close(lfd);
return 0;
}
客戶端代碼:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <string.h> #include <ctype.h> #include <arpa/inet.h> #include <sys/un.h> #include <stddef.h> #include “wrap.h” #define SERV_ADDR “serv.socket” #define CLIE_ADDR “clie.socket” int main(void) { int cfd, len;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
cfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&cliaddr, sizeof(cliaddr));
cliaddr.sun_family = AF_UNIX;
strcpy(cliaddr.sun_path,CLIE_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(cliaddr.sun_path); /* 計算客戶端地址結構有效長度 */
unlink(CLIE_ADDR);
Bind(cfd, (struct sockaddr *)&cliaddr, len); /* 客戶端也需要bind, 不能依賴自動綁定*/
bzero(&servaddr, sizeof(servaddr)); /* 構造server 地址 */
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path, SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* 計算服務器端地址結構有效長度 */
Connect(cfd, (struct sockaddr *)&servaddr, len);
while (fgets(buf, sizeof(buf), stdin) != NULL) {
write(cfd, buf, strlen(buf));
len = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
}
close(cfd);
return 0;
}
107P-本地套接字和網絡套接字實現對比 由于布局原因,直接看課程筆記比較科學。 linux網絡編程資料\day5\1-教學資料\課堂筆記.txt
108P-總結 直接看課程筆記linux網絡編程資料\day5\1-教學資料\課堂筆記.txt
109P-復習
110P-libevent簡介 libevent庫
開源。精簡。跨平臺(Windows、Linux、maxos、unix)。專注于網絡通信。
111P-libevent庫的下載和安裝 源碼包安裝: 參考 README、readme
./configure 檢查安裝環境 生成 makefilemake 生成 .o 和 可執行文件sudo make install 將必要的資源cp置系統指定目錄。進入 sample 目錄,運行demo驗證庫安裝使用情況。編譯使用庫的 .c 時,需要加 -levent 選項。庫名 libevent.so --> /usr/local/lib 查看的到。
特性: 基于“事件”異步通信模型。— 回調。
這里遇到一個問題:
解決辦法: 解決這個問題的博客
完事兒運行測試,結果如下:
112P-libevent封裝的框架思想 libevent框架:
1. 創建 event_base (樂高底座)
2. 創建 事件evnet
3. 將事件 添加到 base上
4. 循環監聽事件滿足
5. 釋放 event_base
創建 event_base (樂高底座)
struct event_base *event_base_new(void);struct event_base *base = event_base_new();
創建 事件evnet
常規事件 event --> event_new(); bufferevent --> bufferevent_socket_new();
將事件 添加到 base上
int event_add(struct event *ev, const struct timeval *tv)
循環監聽事件滿足
int event_base_dispatch(struct event_base *base);event_base_dispatch(base);
釋放 event_base
event_base_free(base);
113P-結合helloworld初識libevent 特性: 基于“事件”異步通信模型?!?回調。
114P-框架相關的不常用函數 查看支持哪些多路IO: 代碼如下:
編譯運行,結果如下:
115P-創建事件對象 創建事件event:
struct event *ev;struct event *event_new(struct event_base *base,evutil_socket_t fd,short what,event_callback_fn cb; void *arg);base: event_base_new()返回值。fd: 綁定到 event 上的 文件描述符what:對應的事件(r、w、e)EV_READ 一次 讀事件EV_WRTIE 一次 寫事件EV_PERSIST 持續觸發。 結合 event_base_dispatch 函數使用,生效。cb:一旦事件滿足監聽條件,回調的函數。typedef void (*event_callback_fn)(evutil_socket_t fd, short, void *) arg: 回調的函數的參數。返回值:成功創建的 event
116P-事件event操作 添加事件到 event_base
int event_add(struct event *ev, const struct timeval *tv);ev: event_new() 的返回值。tv:NULL
銷毀事件
int event_free(struct event *ev);ev: event_new() 的返回值。
117P-使用fifo的讀寫 讀端的代碼如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <fcntl.h> #include <event2/event.h> // 對操作處理函數 void read_cb(evutil_socket_t fd, short what, void *arg) { // 讀管道
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
printf("read event: %s \n", what & EV_READ ? "Yes" : "No");
printf("data len = %d, buf = %s\n", len, buf);
sleep(1);
} // 讀管道 int main(int argc, const char* argv[]) { unlink("myfifo");
//創建有名管道
mkfifo("myfifo", 0664);
// open file
//int fd = open("myfifo", O_RDONLY | O_NONBLOCK);
int fd = open("myfifo", O_RDONLY);
if(fd == -1)
{
perror("open error");
exit(1);
}
// 創建個event_base
struct event_base* base = NULL;
base = event_base_new();
// 創建事件
struct event* ev = NULL;
ev = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);
// 添加事件
event_add(ev, NULL);
// 事件循環
event_base_dispatch(base); // while(1) { epoll();}
// 釋放資源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}
如代碼所示,這個也遵循libevent搭積木的過程
寫管道代碼如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <fcntl.h> #include <event2/event.h> // 對操作處理函數 void write_cb(evutil_socket_t fd, short what, void *arg) { // write管道
char buf[1024] = {0};
static int num = 0;
sprintf(buf, "hello,world-%d\n", num++);
write(fd, buf, strlen(buf)+1);
sleep(1);
} // 寫管道 int main(int argc, const char* argv[]) { // open file
//int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
int fd = open("myfifo", O_WRONLY);
if(fd == -1)
{
perror("open error");
exit(1);
}
// 寫管道
struct event_base* base = NULL;
base = event_base_new();
// 創建事件
struct event* ev = NULL;
// 檢測的寫緩沖區是否有空間寫
//ev = event_new(base, fd, EV_WRITE , write_cb, NULL);
ev = event_new(base, fd, EV_WRITE | EV_PERSIST, write_cb, NULL);
// 添加事件
event_add(ev, NULL);
// 事件循環
event_base_dispatch(base);
// 釋放資源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}
編譯運行,結果如下:
118P-使用fifo的讀寫編碼實現 這個基本上就是把前面代碼寫了一遍,復習一下,問題不大
119P-未決和非未決 未決和非未決:
非未決: 沒有資格被處理未決: 有資格被處理,但尚未被處理event_new --> event ---> 非未決 --> event_add --> 未決 --> dispatch() && 監聽事件被觸發 --> 激活態 --> 執行回調函數 --> 處理態 --> 非未決 event_add && EV_PERSIST --> 未決 --> event_del --> 非未決
120P-中午復習
121P-bufferevent特性
帶緩沖區的事件 bufferevent
#include <event2/bufferevent.h> read/write 兩個緩沖. 借助 隊列.
122P-bufferevent事件對象創建、銷毀 創建、銷毀bufferevent:
struct bufferevent *ev;struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);base: event_basefd: 封裝到bufferevent內的 fdoptions:BEV_OPT_CLOSE_ON_FREE返回: 成功創建的 bufferevent事件對象。void bufferevent_socket_free(struct bufferevent *ev);
123P-給bufferevent事件對象設置回調 給bufferevent設置回調:
對比event: event_new( fd, callback ); event_add() -- 掛到 event_base 上。bufferevent_socket_new(fd) bufferevent_setcb( callback )void bufferevent_setcb(struct bufferevent * bufev,bufferevent_data_cb readcb,bufferevent_data_cb writecb,bufferevent_event_cb eventcb,void *cbarg );bufev: bufferevent_socket_new() 返回值readcb: 設置 bufferevent 讀緩沖,對應回調 read_cb{ bufferevent_read() 讀數據 }writecb: 設置 bufferevent 寫緩沖,對應回調 write_cb { } -- 給調用者,發送寫成功通知。 可以 NULLeventcb: 設置 事件回調。 也可傳NULLtypedef void (*bufferevent_event_cb)(struct bufferevent *bev, short events, void *ctx);void event_cb(struct bufferevent *bev, short events, void *ctx){。。。。。}events: BEV_EVENT_CONNECTEDcbarg: 上述回調函數使用的 參數。read 回調函數類型:typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void*ctx);void read_cb(struct bufferevent *bev, void *cbarg ){.....bufferevent_read(); --- read();}bufferevent_read()函數的原型:size_t bufferevent_read(struct bufferevent *bev, void *buf, size_t bufsize);write 回調函數類型:int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
124P-緩沖區開啟和關閉 啟動、關閉 bufferevent的 緩沖區:
void bufferevent_enable(struct bufferevent *bufev, short events); 啟動 events: EV_READ、EV_WRITE、EV_READ|EV_WRITE默認、write 緩沖是 enable、read 緩沖是 disablebufferevent_enable(evev, EV_READ); -- 開啟讀緩沖。
125P-客戶端和服務器連接和監聽 連接客戶端:
socket();connect();int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen);bev: bufferevent 事件對象(封裝了fd)address、len:等同于 connect() 參2/3
創建監聽服務器:
------ socket();bind();listen();accept();struct evconnlistener * listnerstruct evconnlistener *evconnlistener_new_bind ( struct event_base *base,evconnlistener_cb cb, void *ptr, unsigned flags,int backlog,const struct sockaddr *sa,int socklen);base: event_basecb: 回調函數。 一旦被回調,說明在其內部應該與客戶端完成, 數據讀寫操作,進行通信。ptr: 回調函數的參數flags: LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLEbacklog: listen() 2參。 -1 表最大值sa:服務器自己的地址結構體socklen:服務器自己的地址結構體大小。返回值:成功創建的監聽器。
釋放監聽服務器:
void evconnlistener_free(struct evconnlistener *lev);
126P-libevent實現TCP服務器流程 服務器端 libevent 創建TCP連接:
創建event_base
創建bufferevent事件對象。bufferevent_socket_new();
使用bufferevent_setcb() 函數給 bufferevent的 read、write、event 設置回調函數。
當監聽的 事件滿足時,read_cb會被調用, 在其內部 bufferevent_read();讀
使用 evconnlistener_new_bind 創建監聽服務器, 設置其回調函數,當有客戶端成功連接時,這個回調函數會被調用。
封裝 listner_cb() 在函數內部。完成與客戶端通信。
設置讀緩沖、寫緩沖的 使能狀態 enable、disable
啟動循環 event_base_dispath();
釋放連接。
127P-libevent實現TCP服務器源碼分析 服務器源碼如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <event2/event.h> #include <event2/listener.h> #include <event2/bufferevent.h> // 讀緩沖區回調 void read_cb(struct bufferevent *bev, void *arg) { char buf[1024] = {0};
bufferevent_read(bev, buf, sizeof(buf));
printf("client say: %s\n", buf);
char *p = "我是服務器, 已經成功收到你發送的數據!";
// 發數據給客戶端
bufferevent_write(bev, p, strlen(p)+1);
sleep(1);
} // 寫緩沖區回調 void write_cb(struct bufferevent *bev, void *arg) { printf("I'm服務器, 成功寫數據給客戶端,寫緩沖區回調函數被回調...\n");
} // 事件 void event_cb(struct bufferevent *bev, short events, void *arg) { if (events & BEV_EVENT_EOF)
{
printf("connection closed\n");
}
else if(events & BEV_EVENT_ERROR)
{
printf("some other error\n");
}
bufferevent_free(bev);
printf("buffevent 資源已經被釋放...\n");
} void cb_listener( struct evconnlistener *listener,
evutil_socket_t fd,
struct sockaddr *addr,
int len, void *ptr)
{ printf(“connect new client\n”); struct event_base* base = (struct event_base*)ptr; // 通信操作 // 添加新事件 struct bufferevent *bev; bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); // 給bufferevent緩沖區設置回調 bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL); bufferevent_enable(bev, EV_READ); } int main(int argc, const char* argv[]) { // init server
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(9876);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
struct event_base* base;
base = event_base_new();
// 創建套接字
// 綁定
// 接收連接請求
struct evconnlistener* listener;
listener = evconnlistener_new_bind(base, cb_listener, base,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
36, (struct sockaddr*)&serv, sizeof(serv));
event_base_dispatch(base);
evconnlistener_free(listener);
event_base_free(base);
return 0;
}
128P-服務器注意事項 bufev_server的代碼,錘起來:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <event2/event.h> #include <event2/bufferevent.h> #include <event2/listener.h> #include <pthread.h> void sys_err(const char *str) { perror(str);
exit(1);
} // 讀事件回調 void read_cb(struct bufferevent *bev, void *arg) { char buf[1024] = {0};
// 借助讀緩沖,從客戶端拿數據
bufferevent_read(bev, buf, sizeof(buf));
printf("clinet write: %s\n", buf);
// 借助寫緩沖,寫數據回給客戶端
bufferevent_write(bev, "abcdefg", 7);
} // 寫事件回調 void write_cb(struct bufferevent *bev, void *arg) { printf("-------fwq------has wrote\n");
} // 其他事件回調 void event_cb(struct bufferevent *bev, short events, void *ctx) { } // 被回調,說明有客戶端成功連接, cfd已經傳入該參數內部。 創建bufferevent事件對象 // 與客戶端完成讀寫操作。 void listener_cb(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *addr, int len, void *ptr)
{ struct event_base *base = (struct event_base *)ptr;
// 創建bufferevent 對象
struct bufferevent *bev = NULL;
bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE);
// 給bufferevent 對象 設置回調 read、write、event
void bufferevent_setcb(struct bufferevent * bufev,
bufferevent_data_cb readcb,
bufferevent_data_cb writecb,
bufferevent_event_cb eventcb,
void *cbarg );
// 設置回調函數
bufferevent_setcb(bev, read_cb, write_cb, NULL, NULL);
// 啟動 read 緩沖區的 使能狀態
bufferevent_enable(bev, EV_READ);
return ;
} int main(int argc, char *argv[]) { // 定義服務器地址結構
struct sockaddr_in srv_addr;
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(8765);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 創建event_base
struct event_base *base = event_base_new();
/*
struct evconnlistener *evconnlistener_new_bind (
struct event_base *base,
evconnlistener_cb cb,
void *ptr,
unsigned flags,
int backlog,
const struct sockaddr *sa,
int socklen);
*/
// 創建服務器監聽器:
struct evconnlistener *listener = NULL;
listener = evconnlistener_new_bind(base, listener_cb, (void *)base,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1,
(struct sockaddr *)&srv_addr, sizeof(srv_addr));
// 啟動監聽循環
event_base_dispatch(base);
// 銷毀event_base
evconnlistener_free(listener);
event_base_free(base);
return 0;
}
129P-客戶端流程簡析和回顧
代碼走起:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <event2/bufferevent.h> #include <event2/event.h> #include <arpa/inet.h> void read_cb(struct bufferevent *bev, void *arg) { char buf[1024] = {0};
bufferevent_read(bev, buf, sizeof(buf));
printf("fwq say:%s\n", buf);
bufferevent_write(bev, buf, strlen(buf)+1);
sleep(1);
} void write_cb(struct bufferevent *bev, void *arg) { printf("----------我是客戶端的寫回調函數,沒卵用\n");
} void event_cb(struct bufferevent *bev, short events, void *arg) { if (events & BEV_EVENT_EOF)
{
printf("connection closed\n");
}
else if(events & BEV_EVENT_ERROR)
{
printf("some other error\n");
}
else if(events & BEV_EVENT_CONNECTED)
{
printf("已經連接服務器...\\(^o^)/...\n");
return;
}
// 釋放資源
bufferevent_free(bev);
} // 客戶端與用戶交互,從終端讀取數據寫給服務器 void read_terminal(evutil_socket_t fd, short what, void *arg) { // 讀數據
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
struct bufferevent* bev = (struct bufferevent*)arg;
// 發送數據
bufferevent_write(bev, buf, len+1);
} int main(int argc, const char* argv[]) { struct event_base* base = NULL;
base = event_base_new();
int fd = socket(AF_INET, SOCK_STREAM, 0);
// 通信的fd放到bufferevent中
struct bufferevent* bev = NULL;
bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
// init server info
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(9876);
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
// 連接服務器
bufferevent_socket_connect(bev, (struct sockaddr*)&serv, sizeof(serv));
// 設置回調
bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
// 設置讀回調生效
// bufferevent_enable(bev, EV_READ);
// 創建事件
struct event* ev = event_new(base, STDIN_FILENO, EV_READ | EV_PERSIST,
read_terminal, bev);
// 添加事件
event_add(ev, NULL);
event_base_dispatch(base);
event_free(ev);
event_base_free(base);
return 0;
}
130P-總結 linux網絡編程資料\day6\1-教學資料\課堂筆記.txt
131P-復習
132P-web大練習的概述 寫一個供用戶訪問主機文件的web服務器
133P-HTML文本和標題
134P-HTML文本和標題 和上一話重復,僵硬,跳過
135P-錯誤頁面html 代碼比較簡單
136P-列表、圖片和超鏈接
137P-http協議請求、應答協議基礎格式
138P-服務器框架復習和getline函數 代碼如下:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #define MAXSIZE 2048 int init_listen_fd(int port, int epfd) { // 創建監聽的套接字 lfd
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket error");
exit(1);
}
// 創建服務器地址結構 IP+port
struct sockaddr_in srv_addr;
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口復用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 給 lfd 綁定地址結構
int ret = bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret == -1) {
perror("bind error");
exit(1);
}
// 設置監聽上限
ret = listen(lfd, 128);
if (ret == -1) {
perror("listen error");
exit(1);
}
// lfd 添加到 epoll 樹上
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
perror("epoll_ctl add lfd error");
exit(1);
}
return lfd;
} void do_accept(int lfd, int epfd) { struct sockaddr_in clt_addr;
socklen_t clt_addr_len = sizeof(clt_addr);
int cfd = accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);
if (cfd == -1) {
perror("accept error");
exit(1);
}
// 打印客戶端IP+port
char client_ip[64] = {0};
printf("New Client IP: %s, Port: %d, cfd = %d\n",
inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(clt_addr.sin_port), cfd);
// 設置 cfd 非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 將新節點cfd 掛到 epoll 監聽樹上
struct epoll_event ev;
ev.data.fd = cfd;
// 邊沿非阻塞模式
ev.events = EPOLLIN | EPOLLET;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1) {
perror("epoll_ctl add cfd error");
exit(1);
}
} void do_read(int cfd, int epfd) { // read cfd 小 -- 大 write 回
// 讀取一行http協議, 拆分, 獲取 get 文件名 協議號
} void epoll_run(int port) { int i = 0;
struct epoll_event all_events[MAXSIZE];
// 創建一個epoll監聽樹根
int epfd = epoll_create(MAXSIZE);
if (epfd == -1) {
perror("epoll_create error");
exit(1);
}
// 創建lfd,并添加至監聽樹
int lfd = init_listen_fd(port, epfd);
while (1) {
// 監聽節點對應事件
int ret = epoll_wait(epfd, all_events, MAXSIZE, -1);
if (ret == -1) {
perror("epoll_wait error");
exit(1);
}
for (i=0; i<ret; ++i) {
// 只處理讀事件, 其他事件默認不處理
struct epoll_event *pev = &all_events[i];
// 不是讀事件
if (!(pev->events & EPOLLIN)) {
continue;
}
if (pev->data.fd == lfd) { // 接受連接請求
do_accept(lfd, epfd);
} else { // 讀數據
do_read(pev->data.fd, epfd);
}
}
}
} int main(int argc, char *argv[]) { // 命令行參數獲取 端口 和 server提供的目錄
if (argc < 3)
{
printf("./server port path\n");
}
// 獲取用戶輸入的端口
int port = atoi(argv[1]);
// 改變進程工作目錄
int ret = chdir(argv[2]);
if (ret != 0) {
perror("chdir error");
exit(1);
}
// 啟動 epoll監聽
epoll_run(port);
return 0;
}
139P-復習 請求協議: — 瀏覽器組織,發送
GET /hello.c Http1.1\r\n 2. Host: localhost:2222\r\n 3. User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:24.0) Gecko/201001 01 Firefox/24.0\r\n 4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8\r\n 5. Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3\r\n 6. Accept-Encoding: gzip, deflate\r\n 7. Connection: keep-alive\r\n 8. If-Modified-Since: Fri, 18 Jul 2014 08:36:36 GMT\r\n 【空行】\r\n
應答協議:
Http1.1 200 OK 2. Server: xhttpd Content-Type:text/plain; charset=iso-8859-1 3. Date: Fri, 18 Jul 2014 14:34:26 GMT 5. Content-Length: 32 ( 要么不寫 或者 傳-1, 要寫務必精確 ! ) 6. Content-Language: zh-CN 7. Last-Modified: Fri, 18 Jul 2014 08:36:36 GMT 8. Connection: close \r\n [數據起始。。。。。 。。。。 。。。數據終止]
140P-單文件通信流程分析
getline() 獲取 http協議的第一行。
從首行中拆分 GET、文件名、協議版本。 獲取用戶請求的文件名。
判斷文件是否存在。 stat()
判斷是文件還是目錄。
是文件-- open – read – 寫回給瀏覽器
先寫 http 應答協議頭 : http/1.1 200 ok
Content-Type:text/plain; charset=iso-8859-1
141P-處理出錯返回
142P-正則表達式獲取文件名
void do_read(int cfd, int epfd) { // 讀取一行http協議, 拆分, 獲取 get 文件名 協議號
char line[1024] = {0};
char method[16], path[256], protocol[16];
int len = get_line(cfd, line, sizeof(line)); //讀 http請求協議首行 GET /hello.c HTTP/1.1
if (len == 0) {
printf("服務器,檢查到客戶端關閉....\n");
disconnect(cfd, epfd);
} else {
sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
printf("method=%s, path=%s, protocol=%s\n", method, path, protocol);
while (1) {
char buf[1024] = {0};
len = get_line(cfd, buf, sizeof(buf));
if (buf[0] == '\n') {
break;
} else if (len == -1)
break;
}
}
if (strncasecmp(method, "GET", 3) == 0)
{
char *file = path+1; // 取出 客戶端要訪問的文件名
http_request(cfd, file);
disconnect(cfd, epfd);
}
}
143P-判斷文件是否存在
// 處理http請求, 判斷文件是否存在, 回發 void http_request(int cfd, const char *file) { struct stat sbuf;
// 判斷文件是否存在
int ret = stat(file, &sbuf);
if (ret != 0) {
// 回發瀏覽器 404 錯誤頁面
perror("stat");
exit(1);
}
if(S_ISREG(sbuf.st_mode)) { // 是一個普通文件
// 回發 http協議應答
//send_respond(cfd, 200, "OK", " Content-Type: text/plain; charset=iso-8859-1", sbuf.st_size);
send_respond(cfd, 200, "OK", "Content-Type:image/jpeg", -1);
//send_respond(cfd, 200, "OK", "audio/mpeg", -1);
// 回發 給客戶端請求數據內容。
send_file(cfd, file);
}
}
144P-寫出http應答協議頭
// 客戶端端的fd, 錯誤號,錯誤描述,回發文件類型, 文件長度
void send_respond(int cfd, int no, char *disp, char *type, int len) { char buf[4096] = {0};
sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);
send(cfd, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: %s\r\n", type);
sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
send(cfd, buf, strlen(buf), 0);
send(cfd, "\r\n", 2, 0);
}
145P-寫數據給瀏覽器
// 發送服務器本地文件 給瀏覽器 void send_file(int cfd, const char *file) { int n = 0, ret;
char buf[4096] = {0};
// 打開的服務器本地文件。 --- cfd 能訪問客戶端的 socket
int fd = open(file, O_RDONLY);
if (fd == -1) {
// 404 錯誤頁面
perror("open error");
exit(1);
}
while ((n = read(fd, buf, sizeof(buf))) > 0) {
ret = send(cfd, buf, n, 0);
if (ret == -1) {
perror("send error");
exit(1);
}
if (ret < 4096)
printf("-----send ret: %d\n", ret);
}
close(fd);
}
146P-文件類型區分
147P-錯誤原因及說明 MP3請求錯誤的原因在于,做錯誤判斷時太粗略,errno=EAGAIN或者errno=EINTR時,并不算錯誤,此時繼續執行循環讀取數據就行。 然而原來的程序是直接退出了,所以沒接收到數據。
148P-錯誤頁面展示
錯誤頁面部分的代碼:
void send_error(int cfd, int status, char *title, char *text) { char buf[4096] = {0};
sprintf(buf, "%s %d %s\r\n", "HTTP/1.1", status, title);
sprintf(buf+strlen(buf), "Content-Type:%s\r\n", "text/html");
sprintf(buf+strlen(buf), "Content-Length:%d\r\n", -1);
sprintf(buf+strlen(buf), "Connection: close\r\n");
send(cfd, buf, strlen(buf), 0);
send(cfd, "\r\n", 2, 0);
memset(buf, 0, sizeof(buf));
sprintf(buf, "<html><head><title>%d %s</title></head>\n", status, title);
sprintf(buf+strlen(buf), "<body bgcolor=\"#cc99cc\"><h2 align=\"center\">%d %s</h4>\n", status, title);
sprintf(buf+strlen(buf), "%s\n", text);
sprintf(buf+strlen(buf), "<hr>\n</body>\n</html>\n");
send(cfd, buf, strlen(buf), 0);
return ;
}
直接看完整代碼吧: epoll_server.c
149P-關于瀏覽器請求ico文件
150P-瀏覽器請求目錄
// http請求處理 void http_request(const char* request, int cfd) { // 拆分http請求行
char method[12], path[1024], protocol[12];
sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
printf("method = %s, path = %s, protocol = %s\n", method, path, protocol);
// 轉碼 將不能識別的中文亂碼 -> 中文
// 解碼 %23 %34 %5f
decode_str(path, path);
char* file = path+1; // 去掉path中的/ 獲取訪問文件名
// 如果沒有指定訪問的資源, 默認顯示資源目錄中的內容
if(strcmp(path, "/") == 0) {
// file的值, 資源目錄的當前位置
file = "./";
}
// 獲取文件屬性
struct stat st;
int ret = stat(file, &st);
if(ret == -1) {
send_error(cfd, 404, "Not Found", "NO such file or direntry");
return;
}
// 判斷是目錄還是文件
if(S_ISDIR(st.st_mode)) { // 目錄
// 發送頭信息
send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1);
// 發送目錄信息
send_dir(cfd, file);
} else if(S_ISREG(st.st_mode)) { // 文件
// 發送消息報頭
send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size);
// 發送文件內容
send_file(cfd, file);
}
}
151P-判斷文件類型
// 通過文件名獲取文件的類型 const char *get_file_type(const char *name) { char* dot;
// 自右向左查找‘.’字符, 如不存在返回NULL
dot = strrchr(name, '.');
if (dot == NULL)
return "text/plain; charset=utf-8";
if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
return "text/html; charset=utf-8";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".au") == 0)
return "audio/basic";
if (strcmp( dot, ".wav" ) == 0)
return "audio/wav";
if (strcmp(dot, ".avi") == 0)
return "video/x-msvideo";
if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
return "video/quicktime";
if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
return "video/mpeg";
if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
return "model/vrml";
if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
return "audio/midi";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
if (strcmp(dot, ".ogg") == 0)
return "application/ogg";
if (strcmp(dot, ".pac") == 0)
return "application/x-ns-proxy-autoconfig";
return "text/plain; charset=utf-8";
}
152P-漢字字符編碼和解碼 URL中的漢字默認是存為Unicode碼
153P-libevent實現的web服務器 直接源碼啃起來吧
154P-telnet調試
總結
以上是生活随笔 為你收集整理的Linux网络编程——黑马程序员笔记 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。