天天看點

linux伺服器開發 3 網絡程式設計

文章目錄

      • Socket程式設計
      • 1、套接字
      • 2、網絡位元組序
      • 3、sockaddr資料結構
      • 4、網絡套接字函數
      • 5、半關閉
      • 6、2MSL
        • 6.1 TIME_WAIT狀态的存在有兩個理由:
        • 6.2 該狀态為什麼設計在主動關閉這一方:
      • 7、端口複用
      • 8、TCP異常斷開
        • 心跳檢測機制
        • 設定TCP屬性:基本不用了
      • 網絡名詞術語解析
      • 常見網絡知識面試題
      • 9、高并發伺服器
      • 9.1、select
      • 9.2、poll
      • 93、epoll
      • 10、epoll非阻塞IO
      • 11、epoll反應堆模型
      • 12、線程池并發伺服器
      • 13、UDP
        • 1、TCP和UDP的差別
        • 2、UDP緩沖區
        • 3、C/S模型-UDP
        • 4、UDP廣播
        • 5、UDP多點傳播(多點傳播)
        • 6、分屏軟體
      • 14、本地套接字 domain

Socket程式設計

1、套接字

linux伺服器開發 3 網絡程式設計

Socket本身有“插座”的意思,在Linux環境下,用于表示程序間網絡通信的特殊檔案類型。本質為核心借助緩沖區形成的僞檔案。

  1. socket:IP位址+端口号,唯一辨別網絡通訊中的一個程序
  2. socket成對出現。欲建立連接配接的兩個程序各自有一個socket來辨別,這兩個socket組成的socket pair就唯一辨別一個連接配接。
  3. socket一個描述符指向兩個緩沖區。發送緩沖區、接收緩沖區。

2、網絡位元組序

網絡資料流的位址應這樣規定:先發出的資料是低位址,後發出的資料是高位址。

  • 發送主機通常将發送緩沖區中的資料按記憶體位址從低到高的順序發出,
  • 接收主機把從網絡上接到的位元組依次儲存在接收緩沖區中,也是按記憶體位址從低到高的順序儲存。

TCP/IP協定規定,網絡資料流應采用大端位元組序,即低位址高位元組。計算機用的小端存儲。

  • 大端:低位址 存儲在 高位
  • 小端:低位址 存儲在 低位

原因:在UNIX年代誕生的TCP/IP,也就是IBM公司的大型機主要用的大端存儲。此後在windows後才流行小端存儲,intel架構。

linux伺服器開發 3 網絡程式設計

網絡位元組序和主機位元組序的轉換

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);  # host主機 to net網絡 long型    ip,4位元組
uint16_t htons(uint16_t hostshort); # host主機 to net網絡 short型   端口号,2位元組 
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

htonl(INADDR_ANY): 網絡位址為INADDR_ANY,這個宏表示本地的任意IP位址。   
           
  • 伺服器可能有多個網卡,每個網卡也可能綁定多個IP位址。
  • 這樣設定可以在所有的IP位址上監聽,直到與某個用戶端建立了連接配接時才确定下來到底用哪個IP位址。

IP位址轉換函數

#include <arpa/inet.h>
# p字元串ip(點分十進制式) to轉 net網絡ip
int inet_pton(int af, const char *src, void *dst);
參數:
    af:指定ip位址版本,AF_INET -> ipv4 ;  AF_INET6 -> ipv6
    src:點分十進制 ip,192.168.1.24
    dst:傳出參數
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
參數:
    size: sizeof(dst)
           

3、sockaddr資料結構

  • IPv4和IPv6的位址格式定義在netinet/in.h中
  • IPv4位址用sockaddr_in結構體表示,包括16位端口号和32位IP位址
  • IPv6位址用sockaddr_in6結構體表示,包括16位端口号、128位IP位址和一些控制字段。
  • UNIX Domain Socket的位址格式定義在sys/un.h中,用sock-addr_un結構體表示。
linux伺服器開發 3 網絡程式設計
早期ipv4用的是 strcut sockaddr類型。 
現在 sockaddr類型已經退化為(void *)的作用,用于給函數傳遞位址。
	定義時為:sockaddr_in 或者 sockaddr_in6
	調用函數時:強轉為 (struct sockaddr *)&addr。 bind、accept、connect需要強轉。
#include <arpa/inet.h>
struct sockaddr {
	sa_family_t sa_family; 		/* address family, AF_xxx */
	char sa_data[14];			/* 14 bytes of protocol address */
};

struct sockaddr_in {
	sa_family_t sin_family; 		/* Address family */    協定AF_INET
	in_port_t sin_port;				/* Port number */		端口号
	struct in_addr sin_addr;		/* Internet address */	IP位址
};

struct in_addr{
	uint32_t s_addr;  				# IP位址
}
           

4、網絡套接字函數

linux伺服器開發 3 網絡程式設計
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

1、建立socket
int socket(int domain, int type, int protocol);
傳回值: 成功:socket檔案描述符。 失敗:-1,設定errno
參數:
	domain:	ip位址協定。AF_INET、AF_INET6、AF_UNIX本地套接字
	type:	通信協定。 
		SOCK_STREAM:流式協定。按序、可靠、資料完整、基于位元組流。 使用TCP
		SOCK_DGRAM: 報式協定。無連接配接、固定長度、不可靠。 使用UDP
		其他:SOCK_SEQPACKET、SOCK_RAW、SOCK_RDM
    protocol:  傳 0 表示預設協定。
    
2、将sockfd 綁定 ip和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
傳回值: 成功傳回0,失敗傳回-1, 設定errno
參數:
    sockfd:		socket檔案描述符
    addr:		構造出IP位址加端口号
    addrlen:	sizeof(addr)長度

3、指定監聽上限數,同時用戶端建立連接配接(處于和剛建立三次握手的數量和)。
int listen(int sockfd, int backlog);
參數:
    sockfd: socket檔案描述符
    backlog: 排隊建立3次握手隊列和剛剛建立3次握手隊列的連結數和

4、服務端調用,阻塞等待用戶端連接配接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
傳回值: 成功傳回一個 新的 socket檔案描述符,用于和用戶端通信。失敗傳回-1,設定errno
參數:
    sockdf: socket檔案描述符
    addr:   傳出參數,傳回用戶端socket,服務端不用初始化。
    addrlen: 傳入傳出參數。傳入sizeof(addr)大小,傳出接收到位址結構體的大小。不關心填NULL
    	

5、用戶端調用,建立連接配接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
傳回值: 成功傳回0。 失敗傳回-1,設定errno
參數:
    sockdf:	socket檔案描述符
    addr:  	傳入參數,指定伺服器端位址資訊,含IP位址和端口号C++
    addrlen:傳入參數,傳入sizeof(addr)大小

           

由于用戶端不需要固定的端口号,是以不必調用bind(),用戶端的端口号由核心自動配置設定。

用戶端不是不允許調用bind(),隻是沒有必要調用bind()固定一個端口号,伺服器也不是必須調用bind(),但如果伺服器不調用bind(),核心會自動給伺服器配置設定監聽端口,每次啟動伺服器時端口号都不一樣,用戶端要連接配接伺服器就會遇到麻煩。

用戶端和伺服器啟動後可以使用netstat指令檢視連結情況:
netstat -apn|grep 6666
           
linux伺服器開發 3 網絡程式設計

5、半關閉

#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要關閉的socket的描述符
how:	允許為shutdown操作選擇以下幾種方式:
	SHUT_RD(0):	關閉sockfd上的讀功能,此選項将不允許sockfd進行讀操作。
					該套接字不再接受資料,任何目前在套接字接受緩沖區的資料将被無聲的丢棄掉。
	SHUT_WR(1):		關閉sockfd的寫功能,此選項将不允許sockfd進行寫操作。程序不能在對此套接字發出寫操作。
	SHUT_RDWR(2):	關閉sockfd的讀寫功能。相當于調用shutdown兩次:首先是以SHUT_RD,然後以SHUT_WR。

           

使用close中止一個連接配接,但它隻是減少描述符的引用計數,并不直接關閉連接配接,隻有當描述符的引用計數為0時才關閉連接配接。

shutdown不考慮描述符的引用計數,直接關閉描述符。

注意:

  1. 如果有多個程序共享一個套接字,close每被調用一次,計數減1,直到計數為0時,也就是所用程序都調用了close,套接字将被釋放。
  2. 在多程序中如果一個程序調用了shutdown(sfd, SHUT_RDWR)後,其它的程序将無法進行通信。但,如果一個程序close(sfd)将不會影響到其它程序。

6、2MSL

6.1 TIME_WAIT狀态的存在有兩個理由:

(1)讓4次握手關閉流程更加可靠;4次握手的最後一個ACK是是由主動關閉方發送出去的,若這個ACK丢失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀态,則有更大的機會讓丢失的ACK被再次發送出去。

(2)防止lost duplicate對後續建立正常連結的傳輸造成破壞。lost uplicate在實際的網絡中非常常見,經常是由于路由器産生故障,路徑無法收斂,導緻一個packet在路由器A,B,C之間做類似死循環的跳轉。IP頭部有個TTL,限制了一個包在網絡中的最大跳數,是以這個包有兩種命運,要麼最後TTL變為0,在網絡中消失;要麼TTL在變為0之前路由器路徑收斂,它憑借剩餘的TTL跳數終于到達目的地。但非常可惜的是TCP通過逾時重傳機制在早些時候發送了一個跟它一模一樣的包,并先于它達到了目的地,是以它的命運也就注定被TCP協定棧抛棄。

另外一個概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接配接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,則會對我們的傳輸造成緻命的錯誤。

TCP是流式的,所有包到達的順序是不一緻的,依靠序列号由TCP協定棧做順序的拼接;假設一個incarnation connection這時收到的seq=1000, 來了一個lost duplicate為seq=1000,len=1000, 則TCP認為這個lost duplicate合法,并存放入了receive buffer,導緻傳輸出現錯誤。通過一個2MSL TIME_WAIT狀态,確定所有的lost duplicate都會消失掉,避免對新連接配接造成錯誤。

6.2 該狀态為什麼設計在主動關閉這一方:

(1)發最後ACK的是主動關閉一方。

(2)隻要有一方保持TIME_WAIT狀态,就能起到避免incarnation connection在2MSL内的重建立立,不需要兩方都有。

如何正确對待2MSL TIME_WAIT?

RFC要求socket pair在處于TIME_WAIT時,不能再起一個incarnation connection。但絕大部分TCP實作,強加了更為嚴格的限制。在2MSL等待期間,socket中使用的本地端口在預設情況下不能再被使用。

若A 10.234.5.5 : 1234和B 10.55.55.60 : 6666建立了連接配接,A主動關閉,那麼在A端隻要port為1234,無論對方的port和ip是什麼,都不允許再起服務。這甚至比RFC限制更為嚴格,RFC僅僅是要求socket pair不一緻,而實作當中隻要這個port處于TIME_WAIT,就不允許起連接配接。這個限制對主動打開方來說是無所謂的,因為一般用的是臨時端口;但對于被動打開方,一般是server,就悲劇了,因為server一般是熟知端口。比如http,一般端口是80,不可能允許這個服務在2MSL内不能起來。

解決方案是給伺服器的socket設定SO_REUSEADDR選項,這樣的話就算熟知端口處于TIME_WAIT狀态,在這個端口上依舊可以将服務啟動。當然,雖然有了SO_REUSEADDR選項,但sockt pair這個限制依舊存在。比如上面的例子,A通過SO_REUSEADDR選項依舊在1234端口上起了監聽,但這時我們若是從B通過6666端口去連它,TCP協定會告訴我們連接配接失敗,原因為Address already in use.

RFC 793中規定MSL為2分鐘,實際應用中常用的是30秒,1分鐘和2分鐘等。

RFC (Request For Comments),是一系列以編号排定的檔案。收集了有關網際網路相關資訊,以及UNIX和網際網路社群的軟體檔案。

7、端口複用

服務端主動關閉後,屬于TIME_WAIT 狀态,端口不可用,2MSL後才能用。

在server的TCP連接配接沒有完全斷開之前不允許重新監聽是不合理的。因為,TCP連接配接沒有完全斷開指的是connfd(127.0.0.1:6666)沒有完全斷開,而我們重新監聽的是lis-tenfd(0.0.0.0:6666),雖然是占用同一個端口,但IP位址不同,connfd對應的是與某個用戶端通訊的一個具體的IP位址,而listenfd對應的是wildcard address。

解決這個問題的方法是使用setsockopt()設定socket描述符的選項SO_REUSEADDR為1,表示允許建立端口号相同但IP位址不同的多個socket描述符。

伺服器仍是 TIME_WAIT ,但是端口允許複用。

在server代碼的socket()和bind()調用之間插入如下代碼:
  int opt = 1;
  setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
           

有關setsockopt可以設定的其它選項請參考UNP第7章。

8、TCP異常斷開

在TCP網絡通信中,經常會出現用戶端和伺服器之間的非正常斷開,需要實時檢測查詢連結狀态。

心跳檢測機制

  • 心跳包:檢測是否掉線。乒乓包:心跳包的基礎上可以攜帶少量資訊(微信朋友圈紅點)。
  • Heart-Beat線程:這個是最常用的簡單方法。在接收和發送資料時個人設計一個守護程序(線程),定時發送Heart-Beat包,用戶端/伺服器收到該小包後,立刻傳回相應的包即可檢測對方是否實時線上。
  • 但缺點就是會改變現有的通訊協定!大家一般都是使用業務層心跳來處理,主要是靈活可控。
  • UNIX網絡程式設計不推薦使用SO_KEEPALIVE來做心跳檢測,還是在業務層以心跳包做檢測比較好,也友善控制。

設定TCP屬性:基本不用了

  • SO_KEEPALIVE:如果2小時内在此套接口的任一方向都沒有資料交換,TCP就自動給對方發一個保持存活探測分節(keepalive probe)。這是一個對方必須響應的TCP分節。三種情況:
    • 對方接收一切正常:以期望的ACK響應。2小時後,TCP将發出另一個探測分節。
    • 對方已崩潰且已重新啟動:以RST響應。套接口的待處理錯誤被置為ECONNRESET,套接 口本身則被關閉。
    • 對方無任何響應:源自berkeley的TCP發送另外8個探測分節,相隔75秒一個,試圖得到一個響應。
  • 在發出第一個探測分節11分鐘 15秒後若仍無響應就放棄。套接口的待處理錯誤被置為ETIMEOUT,套接口本身則被關閉。如ICMP錯誤是“host unreachable(主機不可達)”,說明對方主機并沒有崩潰,但是不可達,這種情況下待處理錯誤被置為EHOSTUNREACH。
keepAlive = 1;
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));
           
  • 如果我們不能接受如此之長的等待時間,從TCP-Keepalive-HOWTO上可以知道一共有兩種方式可以設定,一種是修改核心關于網絡方面的 配置參數,另外一種就是SOL_TCP字段的TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT三個選項。
  • SO_KEEPALIVE設定空閑2小時才發送一個“保持存活探測分節”,不能保證明時檢測。對于判斷網絡斷開時間太長,對于需要及時響應的程式不太适應。
  • 當然也可以修改時間間隔參數,但是會影響到所有打開此選項的套接口!關聯了完成端口的socket可能會忽略掉該套接字選項。

網絡名詞術語解析

暫略

常見網絡知識面試題

  1. TCP如何建立連結
  2. TCP如何通信
  3. TCP如何關閉連結
  4. 什麼是滑動視窗
  5. 什麼是半關閉
  6. 區域網路内兩台機器如何利用TCP/IP通信
  7. internet上兩台主機如何進行通信
  8. 如何在internet上識别唯一一個程序

答:通過“IP位址+端口号”來區分不同的服務

  1. 為什麼說TCP是可靠的連結,UDP不可靠
  2. 路由器和交換機的差別
  3. 點到點,端到端

9、高并發伺服器

多路IO轉接伺服器:也叫做多任務IO伺服器。該類伺服器實作的主旨思想是,不再由應用程式自己監視用戶端連接配接,取而代之由核心替應用程式監視檔案。主要使用的方法有三種

核心發送回報。常用的accept函數會阻塞。如果是核心的話,直接回報建立連接配接。 讀寫資料時,也是一樣的,不用阻塞等待用戶端寫入資料。

  • select
  • poll
  • epoll

9.1、select

select的問題:

  1. select能監聽的個數 受限于 檔案描述符個數上限 FD_SETSIZE,一般為1024,單純改變程序打開的檔案描述符個數并不能改變select監聽檔案個數
  2. 解決1024以下用戶端時使用select是很合适的,但如果連結用戶端過多,select采用的是輪詢模型,會大大降低伺服器響應效率,不應在select上投入更多精力
  3. 因為select會修改傳入參數,是以需要額外儲存監聽的集合。
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
			fd_set *exceptfds, struct timeval *timeout);
傳回值:監聽的所有集合中,滿足條件的總數。 失敗 -1,設定errno
參數:
	nfds: 		監控的檔案描述符集裡最大檔案描述符加1,告訴核心檢測前多少個檔案描述符的狀态
	readfds:	監控有讀資料到達檔案描述符集合,傳入傳出參數
	writefds:	監控寫資料到達檔案描述符集合,傳入傳出參數
	exceptfds:	監控異常發生達檔案描述符集合,如帶外資料到達異常,傳入傳出參數
	timeout:	定時阻塞監控時間,3種情況
				1.NULL,永遠等下去
				2.設定timeval,等待固定時間
				3.設定timeval裡時間均為0,檢查描述字後立即傳回,輪詢
	struct timeval {
		long tv_sec; /* seconds */
		long tv_usec; /* microseconds */
	};
檔案描述符集合fd_set 的操作函數:
	void FD_ZERO(fd_set *set); 			//把檔案描述符集合裡所有位清0
	void FD_CLR(int fd, fd_set *set); 	//把檔案描述符集合裡fd清0
	int FD_ISSET(int fd, fd_set *set); 	//測試檔案描述符集合裡fd是否置1
	void FD_SET(int fd, fd_set *set); 	//把檔案描述符集合裡fd位置1
           

9.2、poll

優點:

  • 突破檔案描述符個數1024的限制。改變配置檔案。 sudo vi /etc/security/limits.conf
  • 監聽和傳回集合一緻,分離
  • 定義了一個數組,滿足條件的直接放到數組裡。減少搜素範圍。

缺點:不能跨平台

如果不再監控某個檔案描述符時,可以把pollfd中,fd設定為-1,poll不再監控此pollfd,下次傳回時,把revents設定為0。

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數:
fds:結構體數組的首位址
	struct pollfd {
		int fd; /* 檔案描述符 */
		short events; /* 監控的事件 */
		short revents; /* 監控事件中滿足條件傳回的事件 */
	};
	#
	POLLIN			普通或帶外優先資料可讀,即POLLRDNORM | POLLRDBAND
	POLLRDNORM		資料可讀
	POLLRDBAND		優先級帶資料可讀
	POLLPRI 		高優先級可讀資料
	
	POLLOUT		普通或帶外資料可寫
	POLLWRNORM		資料可寫
	POLLWRBAND		優先級帶資料可寫
	
	POLLERR 		發生錯誤
	POLLHUP 		發生挂起
	POLLNVAL 		描述字不是一個打開的檔案

nfds 			監控數組中有多少檔案描述符需要被監控

timeout 		毫秒級等待
    -1:阻塞等,#define INFTIM -1 				Linux中沒有定義此宏
    0:立即傳回,不阻塞程序
    >0:等待指定毫秒數,如目前系統時間精度不夠毫秒,向上取值

           

93、epoll

epoll是Linux下多路複用IO接口select/poll的增強版本,它能顯著提高程式在大量并發連接配接中隻有少量活躍的情況下的系統CPU使用率,因為它會複用檔案描述符集合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新準備要被偵聽的檔案描述符集合,另一點原因就是擷取事件的時候,它無須周遊整個被偵聽的描述符集,隻要周遊那些被核心IO事件異步喚醒而加入Ready隊列的描述符集合就行了。

  • 當select監聽的幾乎是集合中的所有fd的話,和epoll的性能沒有很大差別。

epoll除了提供select/poll那種IO事件的電平觸發(Level Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得使用者空間程式有可能緩存IO狀态,減少epoll_wait/epoll_pwait的調用,提高應用程式效率。

可以使用cat指令檢視一個程序可以打開的socket描述符上限。
  cat /proc/sys/fs/file-max    # 硬體上限,

如有需要,可以通過修改配置檔案的方式修改該上限值。
  sudo vi /etc/security/limits.conf
# 在檔案尾部寫入以下配置,soft軟限制,hard硬限制。如下圖所示。
  * soft nofile 65536
  * hard nofile 100000
           
linux伺服器開發 3 網絡程式設計

基本API(3個)

#include <sys/epoll.h>

1.	建立一個epoll句柄,參數size用來告訴核心監聽的檔案描述符的個數,跟記憶體大小有關。
int epoll_create(int size)	# size:監聽數目,建議值
傳回值: 檔案描述符(句柄)、指向核心中紅黑樹的樹根節點(二分法查找)

2.	控制某個epoll監控的檔案描述符上的事件:注冊、修改、删除。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
傳回值:成功傳回0,不成功傳回-1
參數:
    epfd:	為epoll_creat的句柄
    op:	表示動作,3:
        EPOLL_CTL_ADD (注冊新的fd到epfd),
        EPOLL_CTL_MOD (修改已經注冊的fd的監聽事件),
        EPOLL_CTL_DEL (從epfd删除一個fd);
    fd:	監控的檔案描述符
    event:	告訴核心需要監聽的事件,結構體

    struct epoll_event {
        uint32_t events; /* Epoll events */  EPOLLIN EPOLLOUT EPOLLERR...
        epoll_data_t data; /* User data variable */
    };
    typedef union epoll_data {
        void *ptr;   		# 泛型指針
        int fd;				# 監控的檔案描述符
        uint32_t u32;		# 32位無符号整數
        uint64_t u64;		# 64位無符号整數
    } epoll_data_t;
events:
    EPOLLIN :	表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉)
    EPOLLOUT:	表示對應的檔案描述符可以寫
    EPOLLPRI:	表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來)
    EPOLLERR:	表示對應的檔案描述符發生錯誤
    EPOLLHUP:	表示對應的檔案描述符被挂斷;
    EPOLLET: 	将EPOLL設為邊緣觸發模式,這是相對于水準觸發而言的
    EPOLLONESHOT:隻監聽一次事件,如果還需要繼續監聽這個socket,
        需要再次把這個socket加入到EPOLL隊列裡

3.	等待所監控檔案描述符上有事件的産生,類似于select()調用。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, 
               int timeout)
傳回值:  成功傳回有多少檔案描述符就緒,時間到時傳回0,出錯傳回-1
參數:
    epfd:		為epoll_creat的句柄
    events:		傳出參數,從核心得到epoll_event的集合,數組。
    maxevents:	告之核心這個events有多大,其值不能大于建立epoll_create()時的size
    timeout:	是逾時時間
        -1:	阻塞
        0:	立即傳回,非阻塞
        >0:	指定毫秒

           

檔案描述符不是指針,而是用fd找到一個指針,再找結構體位址

epoll使用過程:

1、建立一個紅黑樹樹根
int epoll_create(10);

2、讀監聽 lfd 
struct epoll_event evt;   // 建立epoll_event 結構體
evt.events  = EPOPLLIN;	// 設定讀監聽
evt.data.fd = lfd; 		// 可以泛型,這裡直接傳fd。聯合體,隻能一個有效。
epoll_ctl(epfd, EPOLL_STL_ADD, lfd, &evt);  //增加節點到紅黑樹
	// 紅黑樹節點自帶一個結構體,存儲了 EPOLLIN和lfd。讀操作 和 對應fd

3、取回結果
struct epoll_event evts[10];   // 用來傳回監聽的 fd,傳出參數
int ret = epoll_wait(epfd, evts, 10, -1);  // 傳回 滿足事件的fd個數
循環ret次,判斷 fd == lfd 監聽 accept。  如果 fd == cfd 讀取 read。
           

總的server代碼 epoll_concurrent

struct epoll_event tep, ep[5000];   // 建立epoll變量
int listenfd, connfd, sockfd;
listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 建立socket

int opt = 1; // 端口複用,server關閉後輸入TIME_WAIT,端口馬上創新使用
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);

Bind(listenfd, (struct sockaddr *) &servaddr, sizeof servaddr);
Listen(listenfd, 20);

efd = epoll_create(OPEN_MAX);  // 紅黑樹樹根
res = epoll_ctl(efd , EPOLL_STL_ADD, listenfd, &tep);  //增加節點到紅黑樹
while(1){
	int ret = epoll_wait(epfd, evts, 10, -1);  // 傳回 滿足事件的fd個數
	周遊 ret, 
		if(!(ep[i].events & EPOLLIN)) continue; // 不是讀事件,直接傳回
		if(ep[i].data.fd == listenfd)  // 監聽 accept,并且添加cfd到紅黑樹
		else{ 
			sockfd = ep[i].data.fd;  n = read(sockfd, buf, MAXLINE);
			// n = 0,用戶端關閉了 删除cfd,close sockfd
			// n小于0,讀出錯
			// n大于0,轉大寫,write(STDOUT_FILENO, buf, n); writen(sockfd, buf, n);
		}
}

           

10、epoll非阻塞IO

  • epoll ET 邊緣觸發:隻有資料到來才觸發,不管緩存區中是否還有資料。
  • epoll LT 水準觸發:隻要有資料都會觸發。

為什麼要ET模式:

  • 比如對一個視訊檔案,隻想要讀取預覽的圖檔、名字之類的,隻要讀取head就可以,不需要讀所有視訊檔案。此時ET模式可以隻讀前50位元組,後面的就不讀的,等下次請求。
  • 可以設定非阻塞IO。 比如傳過來300位元組,隻讀取了100位元組,如果非阻塞IO可以通過再read時循環的方法繼續讀。如果用LT,需要等待下次IO監聽觸發。
epoll非阻塞IO
1、邊緣觸發
2、用循環的方式讀取資料 
	while((len=read(fd, buf, size))>0)
3、設定檔案描述符屬性為O_NONBLOCK
	flag = fcntl(connfd, F_GETFL);  # F_GETFL擷取檔案描述符屬性
	flag |= O_NONBLOCK;				# 修改檔案描述符屬性
	fcntl(connfd, F_SETFL, flag);	# F_SETFL設定檔案描述符屬性
	
使用水準觸發的阻塞IO可能造成死鎖
1、比如需要500B的資料,但是隻有200B的資料。此時阻塞在read函數上。
2、但是又無法去用epoll監聽用戶端新寫的資料。造成了死鎖。
           

11、epoll反應堆模型

使用 libevent 跨平台開發庫。

  • epoll反應堆模型,設定監聽寫事件,防止網絡阻塞。
  • 逾時驗證,逾時将用戶端從紅黑樹移除。
  1. epoll – 伺服器 – 監聽 – cfd --可讀 – epoll傳回 – read – 小寫轉大寫 – write – epoll繼續監聽。
  2. epoll 反應堆模型:
  • 用戶端不一定可寫,比如"滑動視窗",是以需要判斷

    epoll – 伺服器 – 監聽 – cfd – 可讀 – epoll傳回 – read – cfd從樹上摘下 – 設定監聽cfd寫事件 — 小寫轉大寫 – 等待epoll_wait 傳回可寫 — 回寫用戶端 – cfd從樹上摘下 – 設定監聽cfd讀事件 – epoll繼續監聽。

  • epoll事件不再傳fd而是傳泛型參數,用結構體把fd、參數指針、回調函數指針一起傳過去。
  • 存儲fd加入到紅黑樹的時間,如果長時間不幹事就剔除。 last_active = time(NULL) 絕對時間
linux伺服器開發 3 網絡程式設計
/* 描述就緒檔案描述符相關資訊 */

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

main内部:
初始化
0、g_efd = epoll_create(MAX_EVENTS+1)  建立紅黑樹樹根
	epoll_event 數組,節點
1、initlistensocket(g_efd, port);  建立listen fd,并設定為非阻塞。 
	socket建立、fcntl非阻塞、
	
	設定 lfd 的回調函數和參數
	// 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]);
	// g_events數組的最後一個給lfd用。 并設定回調函數 acceptconn,參數為結構體本身。
	// 會初始化 last_active 時間,表示 lfd 運作開始

	添加 lfd 到紅黑樹上
	// void eventadd(int efd, int events, struct myevent_s *ev) 
    eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
    // 會設定 staus,表示節點是否在樹上
    
	bind、listen、
	
循環:
1、如果一直不幹活就剔除
	擷取目前事件
	一次檢視一百個連接配接,判斷 last_active 時間是不是大于60s,是則 eventdel
	每次 eventset 都會更新 last_active 
2、int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000); // 阻塞監聽1s
	循環nfd 。 
		// 用 events[i].data.ptr泛型指針保持 myevent_s 結構體
		struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr; 
		
		// 實際上由兩個判斷,源碼帶有一些相關的瑣碎操作。call_back函數不同
		判斷讀寫事件: // 調用回調函數
			ev->call_back(ev->fd, events[i].events, ev->arg);
3、lfd 回調函數 acceptconn:
	void acceptconn(int lfd, int events, void *arg)
	cfd = accept(lfd, (struct sockaddr *)&cin, &len)  // 擷取cfd
	周遊g_events,找到第一個 status = 0 的位置加入cfd 
	cfd 設定非阻塞
	eventset 設定回調函數 recvdata、eventadd 加到樹上
4、cfd 回調函數 recvdata:
	- 讀取資料,recv(scoket_fd, buf, len, flag=0);  // 讀套接字
	- 删除讀監聽,eventdel(g_efd, ev);    // status狀态變成0
	- 如果讀取資料大于0,轉大寫,ev->buf[len]
	- 設定寫監聽,epoll_set 回調函數 senddata、epoll_add 加樹
	- 出錯,關閉fd。
5、fd 回調函數 senddata,從樹上摘下寫事件,寫資料,挂讀事件


補充:do-while實作goto語句:
do{
	// do sth1
	break;
	// do sth2
}while(0);
- 如果sth1後執行break,sth2的語句不會執行。
- 由于while是0,是以不會循環。
           

回射伺服器

12、線程池并發伺服器

linux伺服器開發 3 網絡程式設計
  • 之前的内容在右邊,也就是接入IO的部分。
  • 線程池技術用于右邊。原本是每一個IO請求,啟動一個線程去工作,結束後銷毀。線程的建立和銷毀需要資源。耗時,對用戶端請求響應不及時。
    linux伺服器開發 3 網絡程式設計
  1. 任務隊列用條件變量控制。
  2. 條件變量 任務隊列不為空的時候,激活線程池來取任務。
  3. 條件變量 任務隊列不為空時,IO線程可以接任務。
  4. 任務隊列要用鎖控制。
  5. 線程池維護的變量:最小線程數、最大線程數、線程控制步長、目前線程存活數、忙線程數。
  6. 設定管理線程:進行 線程數量的控制。
// 任務隊列 中存的 單個任務資訊
typedef struct {
    void *(*function)(void *);          /* 函數指針,回調函數 */
    void *arg;                          /* 上面函數的參數 */
} threadpool_task_t;                    /* 各子線程任務結構體 */

/* 描述線程池相關資訊 */
struct threadpool_t {
    pthread_mutex_t lock;               /* 用于鎖住本結構體 */    
    pthread_mutex_t thread_counter;     /* 記錄忙狀态線程個數的瑣 -- 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 */
};

0、main任務
	/*建立線程池 thp ,池裡最小3個線程,最大100,隊列最大100*/
	threadpool_t *thp = threadpool_create(3,100,100);
	/* 向線程池中添加任務 */     num[i] 為 process的參數
	threadpool_add(thp, process, (void*)&num[i]);
	。。。
	threadpool_destroy(thp);  // 銷毀線程池
1、初始化線程池:
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
	初始化結構體的參數。  threadpool_t *pool 
	malloc并menset最大線程id空間。數組下标通路,需要是連續的。
	malloc最大任務隊列空間。
	初始化鎖 lock、thread_counter。條件變量 queue_not_empty,queue_not_full
	建立線程,pthread_create。
		工作線程,其對應的回調函數  threadpool_thread
		管理者線程,其對應的回調函數  adjust_thread
	傳回線程池結構體 pool 。
	中途出錯,一個個free掉 線程池結構體 内的成員。
2、工作線程:
void *threadpool_thread(void *threadpool)
	加結構體鎖。
		while 沒有任務
			将線程阻塞在條件變量queue_not_empty上。等待
			一旦有任務,條件變量 queue_not_empty 喚醒,跳出阻塞。
			判斷是否需要砍線程。
			有任務了 跳出 while
		任務隊列出隊,設定回調函數和參數。
		廣播條件變量 queue_not_full,表示可以增加任務。
	釋放結構體鎖。
	加thread_counter鎖。設定工作線程數加一。解thread_counter鎖。
	調用回調函數。
	加thread_counter鎖。設定工作線程數減一。解thread_counter鎖。
3、管理者線程:
void *adjust_thread(void *threadpool)
	睡眠固定時間,不需要一直控制線程池。
	加結構體鎖。
		變量另存 目前線程數、存活線程數。
	釋放結構體鎖。
	加 thread_counter 鎖
		另存 忙着的線程數。
	解 thread_counter 鎖
	線程池增加算法:任務數 大于 最小線程池個數, 且 存活的線程數 少于 最大線程個數時
	銷毀空閑線程算法:忙線程X2 小于 存活的線程數 且 存活的線程數 大于 最小線程數時
	銷毀線程:發送 queue_not_empty 條件變量,通知處在空閑狀态的線程自行終止
4、向線程增加任務。
int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
	加結構體鎖。
		任務隊滿,阻塞等待 條件變量 queue_not_full 。
		清空工作線程 調用的回調函數 的參數arg
		添加任務到任務隊列裡
		任務隊列不為空,喚醒一個線程池中的任務線程。
				pthread_cond_signal - queue_not_empty
	解結構體鎖。
4、線程池退出:
int threadpool_destroy(threadpool_t *pool)
	結構體shutdown = true。
	先銷毀管理線程。pthread_join(pool->adjust_tid, NULL);
	通知所有的空閑線程銷毀。pthread_cond_broadcast - queue_not_empty
	銷毀工作線程:pthread_join(pool->threads[i], NULL);
	釋放線程池空間(隊列、鎖、條件變量等)


           

13、UDP

1、TCP和UDP的差別

TCP: 面向連接配接的可靠資料包傳遞 —完全彌補

​ 優點:穩定:

  1. 資料穩定 — 丢包回傳(回執機制)(丢包率97‰)
  2. 速率穩定
  3. 流量穩定 “滑動視窗”

    缺點: 效率低、速度慢

    使用場景:大檔案、重要檔案傳輸

UDP: 無連接配接的不可靠封包傳遞。----完全不彌補

​ 缺點:不穩定:資料、速率、流量

​ 優點:效率高、速度快

​ 使用場景:對實時性要求較高,視訊會議、視訊電話、廣播、飛秋

實際運用: TCP — TCP+UDP — UDP + 應用層自定義協定彌補UDP的丢包。

2、UDP緩沖區

與TCP類似的,UDP也有可能出現緩沖區被填滿後,再接收資料時丢包的現象。由于它沒有TCP滑動視窗的機制,通常采用如下兩種方法解決:

  1. 伺服器應用層設計流量控制,控制發送資料速度。
               
  2. 借助setsockopt函數改變接收緩沖區大小。如:
               
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
	int n = 220x1024
	setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

           

3、C/S模型-UDP

linux伺服器開發 3 網絡程式設計

由于UDP不需要維護連接配接,程式邏輯簡單了很多,但是UDP協定是不可靠的,保證通訊可靠性的機制需要在應用層實作。

ssize_t recv(int sockfd, void *buf, size_t len, int flags); # flags預設傳0
# 用這個,接收socket傳來的資料
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
	struct sockaddr *src_addr, socklen_t *addrlen);  # src_addr傳出,addrlen傳入傳出
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

ssize_t send(int sockfd, void *buf, size_t len, int flags);
# 用這個,回傳資料
ssize_t sendto(int sockfd, void *buf, size_t len, int flags
	const struct sockaddr *dest_addr, socklen_t *addrlen);  # dest_addr傳入
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

           

4、UDP廣播

​ IP:192.168.42.255(廣播) --32位 255 255.255.255.255

​ IP:192.168.42.1(網關)

​ 預設不可以廣播,需要給sockfd開放廣播權限。

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

#define SERVER_PORT 8000           /* 無關緊要 */
#define MAXLINE 1500

#define BROADCAST_IP "192.168.42.255"
#define CLIENT_PORT 9000          # 重要

# 給sockfd開放廣播權限。
int flag = 1; # 表示允許
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sozeof(flag));

# 構造 client 位址 IP+端口  192.168.7.255+9000
bzero(&clientaddr, sizeof(clientaddr));
clientaddr.sin_family = AF_INET;
inet_pton(AF_INET, BROADCAST_IP, &clientaddr.sin_addr.s_addr);
clientaddr.sin_port = htons(CLIENT_PORT);

sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&clientaddr, sizeof(clientaddr));

           

5、UDP多點傳播(多點傳播)

多點傳播組可以是永久的也可以是臨時的。多點傳播組位址中,有一部分由官方配置設定的,稱為永久多點傳播組。永久多點傳播組保持不變的是它的ip位址,組中的成員構成可以發生變化。永久多點傳播組中成員的數量都可以是任意的,甚至可以為零。那些沒有保留下來供永久多點傳播組使用的ip多點傳播位址,可以被臨時多點傳播組利用。

224.0.0.0~224.0.0.255   為預留的多點傳播位址(永久組位址),位址224.0.0.0保留不做配置設定,其它位址供路由協定使用;
224.0.1.0~224.0.1.255   是公用多點傳播位址,可以用于Internet;欲使用需申請。
224.0.2.0~238.255.255.255 為使用者可用的多點傳播位址(臨時組位址),全網範圍内有效;
239.0.0.0~239.255.255.255 為本地管理多點傳播位址,僅在特定的本地範圍内有效。

擷取網卡編号,
1、ip ad指令
2、if_nametoindex 指令可以根據網卡名,擷取網卡序号。
unsigned int if_nametoindex(const char *ifname);   # eth0

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
server:擷取多點傳播權限。
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof(group));
client:将本用戶端加入多點傳播。
setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));

           

​ setsockopt作用:

1. 端口複用。
	2. 設定緩沖區大小
	3. 開放廣播權限
	4. 開放多點傳播權限
	5. 加入多點傳播組。
           

6、分屏軟體

實作基本思路:

1. 螢幕截圖子產品。  24幀
	2. 截取幀數  8-12幀  
	3. 壓縮圖檔 M --> K  
	4. 壓縮資料包
	5. 傳遞 - 多點傳播
	6. 解壓縮 --- 算法。	
	7. 成像
           

14、本地套接字 domain

1. Pipe fifo   實作最簡單
2. mmap 	非血緣關系程序間
3. 信号		開銷小
4. domain	穩定性最好
           

網絡socket也可用于同一台主機的程序間通訊(通過loopback位址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要經過網絡協定棧,不需要打包拆包、計算校驗和、維護序号和應答等。

IPC機制本質上是可靠的通訊,而網絡協定是為不可靠的通訊設計的。UNIX Domain Socket也提供面向流和面向資料包兩種API接口,類似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不會丢失也不會順序錯亂。

UNIX Domain Socket是全雙工的,API接口語義豐富,相比其它IPC機制有明顯的優越性,目前已成為使用最廣泛的IPC機制,比如X Window伺服器和GUI程式之間就是通過UNIX Domain Socket通訊的。

使用UNIX Domain Socket的過程和網絡socket十分相似,也要先調用socket()建立一個socket檔案描述符,address family指定為AF_UNIX,type無所謂,可以選擇SOCK_DGRAM或SOCK_STREAM,protocol參數仍然指定為0即可。

UNIX Domain Socket與網絡socket程式設計最明顯的不同在于位址格式不同,用結構體sockaddr_un表示,網絡程式設計的socket位址是IP位址加端口号,而UNIX Domain Socket的位址是一個socket類型的檔案在檔案系統中的路徑,這個socket檔案由bind()調用建立,如果調用bind()時該檔案已存在,則bind()錯誤傳回。

對比網絡套接字位址結構和本地套接字位址結構:
struct sockaddr_in {
    __kernel_sa_family_t sin_family;	/* Address family */	位址結構類型
    __be16 sin_port;					/* Port number */		端口号
    struct in_addr sin_addr;			/* Internet address */	IP位址
};
struct sockaddr_un {
    __kernel_sa_family_t sun_family;	/* AF_UNIX */	位址結構類型
    char sun_path[UNIX_PATH_MAX];		/* pathname */	socket檔案名(含路徑)
};


将UNIX Domain socket綁定到一個位址
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
# offsetof(type, member) --- ((int)&((type *)0)->MEMBER)
socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un serv_addr;
serv_addr.sun_family = AF_UNIX;
strcpy(serv_addr.sun_path, "mysocket")
int len = offsetof(struct sockaddr_un, sun_path) + strlen(serv_addr.sun_path);
unlink("mysocket");
bind(sfd, (struct sockaddr *)&serv_addr, len);

len長度說明
對于結構體:
struct sockaddr_un{
	sun_family;	# 18bits=2B
	path; 		# 108B,并不都是有效資料
}
如果直接sizeof(sockaddr_un) = 110B
服務端:
用 	offsetof(struct sockaddr_un, sun_path)  = 2B,應該用函數得到
	strlen(un.sun_path);   # 總共是108B,實際不是。如果path是 “mysocket”,則是8B
總 len = 2+8 = 10B
accept(sfd, &client_addr, &len_c);
用戶端:
len_c -= offsetof();  # 得到用戶端socket的檔案名長度
buf[len_c] = '\0';

           

繼續閱讀