
公衆号:暢遊碼海 這裡有更多高品質原創文章和大量免費學習資料!
主機位元組序和網絡位元組序:
在32位機器上,累加器一次能裝載4個位元組,這四個位元組在記憶體中排列順序将影響它被累加器裝載成的整數的值
大端位元組序(網絡位元組序):一個整數的高位位元組存儲在記憶體的低位址處
小端位元組序(現代PC大多數采用):整數的高位位元組存儲在記憶體的高位址處
即使是同一台機器上不同語言編寫的程式通信,也要考慮位元組序的問題
Linux下位元組序轉換函數:
#include<netinet/in.h>
unsigned long int htol (unsigned long int hostlong); //主機位元組序轉換成網絡位元組序
unsigned short int htons (unsigned short int hostshort);//主機位元組序轉換成網絡位元組序
unsigned long int ntohl (unsigned long int netlong);//網絡位元組序轉換成主機位元組序
unsigned short int ntohs (unsigned short int netshort);//網絡位元組序轉換成主機位元組序
socket位址
#include<bits/sockets.h>
struct sockaddr{
sa_family_t sa_family; //位址族類型的變量與協定族對應
char sa_data[14]; //存放socket位址值
}
協定族 | 位址族 | 描述 | 位址值含義和長度 |
PF_UNIX | AF_UNIX | UNIX本地域協定族 | 檔案的路徑名,長度可達108位元組 |
PF_INET | AF_INET | TCP/IPv4協定族 | 16bit端口号和32bit IPv4位址,6位元組 |
PF_INET6 | AF_INET6 | TCP/IPv6協定族 | 16bit端口号,32bit流辨別,128bit IPv6位址,32bit範圍ID,共26位元組 |
為了容納多數協定族位址值,Linux重新定義了socket位址結構體
#include<bits/socket.h>
struct sockaddr_storage{
sa_family_t sa_family;
unsigned long int __ss_align; //是記憶體對齊的
char __ss_padding[128-sizeof(__ss_align)];
}
Linux為TCP/IP協定族有sockaddr_in和sockaddr_in6兩個專用socket位址結構體,它們分别用于IPv4和IPv6
//對于IPv4的:
struct sockaddr_in{
sa_family sin_family; //位址族:AF_INET
u_int16_t sin_port; //端口号,要用網絡位元組序表示
struct in_addr sin_addr;//IPv4位址結構體
}
//IPv4的結構體
struct in_addr
{
u_int32_t s_addr; //要用網絡位元組序表示
}
//對于IPv6
struct sockaddr_in6{
sa_family_t sin6_family;//AF_INET6
u_int16_t sin6_port; //端口号,要用網絡位元組序表示
u_int32_t sin6_flowinfo;//流資訊,應設定為0
struct in6_addr sin6_addr;//IPv6位址結構體
u_int32_t sin6_scope_id;//scope ID,處于試驗階段
}
//IPv6的結構體
struct in6_addr
{
unsigned char sa_addr[16]; //要用網絡位元組序表示
}
使用的時候要強制轉換成通用的socket位址類型socketaddr
點分十進制字元串表示的IPv4位址和網絡位元組序整數表示的IPv4位址轉換
#incldue<arpa/inet.h>
in_addr_t inet_addr(const char* strptr); //點分十進制--->網絡位元組序整數 ,失敗傳回INADDR_NONE
int inet_aton (const char* cp,struct in_addr* inp);//功能同上,結果存儲于參數inp指向的位址結構中,成功傳回1,失敗傳回0
char* inet_ntoa (struct in_addr in); //網絡位元組序整數--->點分十進制,函數内部用靜态變量存儲轉化結果,傳回值指向該變量,inet_ntoa是不可重入的
//功能同上,可用于IPv6
#include<arpa/inet.h>
int inet_pton(int af,const char* src,void* dst);//把結果存放在dst所指記憶體中,其中af代表協定族----成功傳回1,失敗傳回0并且設定error
const char* inet_ntop(int af,const void* src,char* dst,socklen_t cnt);//同理
//下面兩個宏可幫助我們指定cnt的大小
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
建立socket
Linux上所有東西都是檔案
#include<sys/types.h>
#include<sys/socket.h>
int socket (int domain,int type ,int protocol);//domain參數代表底層協定族(IPv4使用PF_INET)、Type參數指定服務類型分為SOCK_STREAM服務(流伺服器--使用TCP協定)和SOCK_DGRAM服務(資料報服務--使用UDP協定)、protocol參數是在前兩個參數構成的協定集合下,再選擇一個具體的協定(幾乎所有情況下它設定0,表示使用預設協定)
socket系統調用成功時傳回一個socket檔案描述符,失敗則傳回-1并設定errno
命名socket
- 建立了socket,并且指定了位址族,但是并沒有指定使用位址族中具體socket位址
- 将一個socket與socket位址綁定稱為給socket命名
- 用戶端通常不需要命名socket,而是采用匿名方式,即使用作業系統自動配置設定的socket位址
#include<sys/types.h>
#include<sys/socket.h>
int bind (int sockfd,const struct sockaddr* my_addr,socklen_t addrlen)//bind将my_addr所指的socket位址配置設定給未命名的sockfd檔案描述符,addrlen參數指出該socket位址的長度,bind成功傳回0,失敗傳回-1并設定errno
兩種常見的errno是EACCES和EADDRINUSE
- EACCCES:被綁定的位址是受保護的位址,僅超級使用者能通路。
- EADDRINUSE: 被綁定的位址正在使用中(例如将socket綁定到一個處于TIME_WAIT狀态的socket位址)
監聽socket
命名後,還不能馬上接受客戶連接配接,我們需要使用如下系統調用來建立一個監聽隊列以存放待處理的客戶連接配接
#include<sys/socket.h>
int listen (int sockfd,int backlog);//sockfd參數指定被監聽的socket,backlog參數提示核心監聽隊列的最大長度,監聽隊列的長度如果超過backlog,伺服器将不再受理新的客戶連接配接,用戶端也将收到ECONNREFUSED錯誤資訊
核心版本2.2之前 :backlog參數是指多有處于半連接配接的狀态(SYN_RCVD)和完全連接配接狀态(ESTABLISHED)的socket的上限
核心版本2.2之後:它隻表示處于完全連接配接狀态的socket的上線,處于半連接配接狀态的socket上限,則是在tcp_max_syn_backlog核心參數定義。
backlog參數的典型值是5,listen成功時傳回0,失敗則傳回-1并設定errno
接受連接配接
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd , struct sockaddr *addr,socklen_t *addrlen);//sockfd參數是執行過listen系統調用的監聽socket。addr參數用來擷取被接受連接配接的遠端socket位址,該socket位址的長度由addrlen參數指出。accept成功時傳回一個新的連接配接socket,該socket唯一辨別了被接受的這個連接配接,伺服器可通過讀寫該socket來與被接受連接配接對應的用戶端通信。失敗時傳回-1,并設定了errno。
發起連接配接
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_adr,socklen_t addrlen);//sockfd參數是socket系統調用傳回一個socket,serv_addr參數是伺服器監聽的socket位址,addrlen參數則是指定
connect成功時傳回0,一旦成功建立連接配接,sockfd就唯一的辨別了這個連接配接,用戶端就可以通過讀寫sockfd來與伺服器通信。失敗傳回-1并設定errno
ECONNREFUSED: 目标端口不存在,連接配接被拒絕ETIMEDOUT: 連接配接逾時
關閉連接配接
#include<unistd.h>
int close(int fd); //fd參數是待關閉的socket,不過并不是立即關閉連接配接,而是将fd的引用計數減一,當為0時,才真正關閉連接配接
多程序程式中,一次系統調用将預設使父程序中打開的socket的引用計數加1,是以我們必須在父程序和子程序中都對該socket執行close調用才能将連接配接關閉
如果無論如何都要立即終止連接配接,可以使用shutdown系統調用
#include<sys/socket.h>
int shutdown (int sockfd,int howto);//sockfd參數是待關閉的socket,howto參數決定了shutdown的行為
可選值 | 含義 |
SHUT_RD | 關閉sockfd上讀的這一半。應用程式不再針對socket檔案描述符執行讀操作,并且該socket接收緩沖區中的資料都被丢棄 |
SHUT_WR | 關閉sockfd上寫的這一半。sockfd的發送緩沖區中的資料會真正關閉連接配接之前全部發送出去,應用程式不可再對該sockfd檔案描述符執行寫操作。這種情況下,連接配接處于半連接配接狀态 |
SHUT_RDWR | 同時關閉sockfd上讀和寫 |
shutdown能夠分别關閉sockfd上的讀和寫,或者都關閉。而close在關閉連接配接時隻能将sockfd上的讀和寫同時關閉
shutdown成功時傳回0,失敗則傳回-1并設定errno
資料讀寫
tcp 資料讀寫
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv (int sockfd , void *buf ,size_t len ,int flags); //recv讀取sockfd上的資料,buf和len參數分别指定讀緩沖區的位置和大小,flags參數的含義見後文,通常設定為0即可。 成功傳回實際讀取到的資料的長度,它可能小于我們期望的長度len。是以我們可能要多次調用。 傳回0,這意味着通信對方已經關閉連接配接了,出錯時傳回-1,并設定errno。
ssize_t send (int sockfd , const void *buf ,size_t len,int flags);//send往sockfd上寫入資料,buf和len依然是緩存區的位置和大小。send成功時傳回實際寫入的長度,失敗則傳回-1,并設定errno。
flags
參數提供額外的控制
flags參數值
UDP資料讀寫
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom (int sockfd ,void* buf , size_t len, int flags , struct sockaddr* src_addr ,socklen_t* addrlen);//recvfrom讀取sockfd上的資料,buf和len參數分别指定讀緩沖區的位置和大小,因為UDP通信沒有連接配接的概念,是以我們每次讀取資料都需要擷取發送端的socket位址,即參數src_addr所指的内容,addrlen參數則指定該位址的長度
ssize_t sendto (int sockfd , const void* buf ,size_t len,int flags ,const struct sockaddr* dest_addr, socklen_t addrlen );// sendto往sockfd上寫入資料,buf和len參數分别指定寫緩沖區的位置和大小。dest_addr參數指定接收端的socket位址,addrlen參數則指定該位址的額長度
//flag含義同上
這兩個也可用于面向連接配接的socket的資料讀寫,隻需要把最後兩個參數都設定為NULL以忽略發送端/接收端的socket位址(已經建立連接配接了,就知道socket位址了)
通用資料讀寫的函數
#include<sys/socket.h>
ssize_t recvmsg (int sockfd, struct msghdr* msg ,int flags);
ssize_t sendmsg (int sockfd ,struct msghdr* msg,int flags);
//msghdr結構體
struct msghdr
{
void* msg_name; //socket位址 對于TCP連接配接這個沒有,因為位址已經知道了
socklen_t msg_namelen;//socket位址的長度
struct iovec* msg_lov;//分散的記憶體塊 //封裝了位置和大小 //數組
int msg_iovlen;//分散的記憶體塊數量
void* msg_control;//指向輔助資料的起始位置
socllen_t msg_controllen;//輔助資料的大小
int msg_flags;//指派函數中的flags參數,并在調用過程中更新
}
struct iovec{
void *iov_base; //記憶體起始位址
size_t iov_len; //記憶體塊的長度
}
對于recvmsg來說,資料将被讀取并存放在msg_iovlen塊分散的記憶體中,這些記憶體的位置和長度則由msg_iov指向的數組指定,這稱為分散讀;對于sendmsg而言,msg_iovlen塊分散記憶體中的資料将被一并發送,這稱為集中寫
帶外标記
#include<sys/socket.h>
int sockatmark (int sockfd);//判斷sockfd是否處于帶外标記,即下一個被讀取的的資料是否是帶外資料。是則傳回1,此時可利用帶MSG_OOB标志的recv調用來接收帶外資料,不是則傳回0
位址資訊函數
#include<iosstream>
int getsockname (int sockfd,struct sockaddr* address, socklen_t* address_len);//擷取本端sockfd位址,并存儲于address參數指定的記憶體中,長度存儲在address_len參數指定的變量中,實際長度大于address所指記憶體區的大小,那麼該socket位址将被截斷。成功傳回0,失敗傳回-1,并設定errno
int getpeername (int sockfd, struct sockaddr* address , socklen_t* address_len);//擷取sockfd對應的遠端socket位址
socket選項
#include<sys/socket.h>
int getsockopt (int sockfd,int level,int option_name , void* option_value);//sockfd參數指定被操作的目标socket。level參數指定要操作哪個協定的選項,option_name參數則指定選項的名字 ,option_value和option_len參數分别是被操作選項的值和長度
int setsockopt (int sockfd , int level ,int option_name ,const void* option_value,socklen_t option_len);
兩個函數成功傳回0 ,失敗傳回-1并設定errno
socket選項
網絡資訊API
//根據主機名,擷取主機的完整資訊
#include<neidb.h>
struct hostent* gethostbyname (const char* name);
struct hostent* gethostbyaddr (const void* addr ,size_t len, int type);
#include<netdb.h>
struct hostent{
char* h_name; //主機名
char** h_aliases;//主機名稱清單,可能由多個
int h_addrtype; //位址類型(位址族)
int h_length; //位址長度
char** h_addr_list;//按網絡位元組序列出的主機IP位址清單
}
//根據名稱擷取某個伺服器的完整資訊
#include<netdb.h>
struct servent* getservbyname (const char* name,const char* proto);
struct servent* getservbyport (int port ,const char* proto);
#include<netdb.h>
struct servent{
char* s_name;//服務名稱
char** s_aliases;//服務的别名清單,可能多個
int s_port;//端口号
char* s_proto;//服務類型,通常是TCP或者UDP
}
//通過主機名擷取IP位址,也能通過服務名獲得端口号----内部使用的是geihostbyname和getservbyname
#include<netdb.h>
int getaddrinfo (const char* hostname ,const char* service ,const struct addrinfo* hints ,struct addrinfo** result);
struct addrinfo
{
int ai_flags;
int ai_family; //位址族
int ai_socktype;//服務類型,SOCK_STREAM或SOCK_DGRAM
int ai_protocol;
socklen_t ai_addrlen;//socket位址ai_addr的長度
char* ai_canonname;//主機的别名
struct sockaddr* ai_addr;//指向socket位址
struct addrinfo* ai_next;//指向下一個sockinfo結構的對象
}
該函數将隐式的配置設定堆記憶體,是以我們需要配對下面的函數
//用來釋放記憶體
#include<netdb.h>
void freeaddrinfo (struct addrinfo* res);
//将傳回的主機名存儲在hsot參數指向的緩存中,将服務名存儲在serv參數指向的緩存中,hostlen和servlen參數分别指定這兩塊緩存的長度
#include<netdb.h>
int getnameinfo (const struct sockaddr* sockaddr,socklen_t addrlen,char* host,socklen_t hostlen,char* serv,socklen_t servlen,int flags);
getnameinfo的flags
getaddrinfo錯誤碼
Part1六、進階I/O函數
//pipe函數可用于建立一個管道,以實作程序間通信
#include<unistd.h>
int pipe( int fd[2]);//參數是一個包含兩個int型整數的數組指針,函數成功時傳回0,并将打開的檔案描述符值填入其參數指向的數組,失敗則傳回-1并設定errno
//fd[0]隻能從管道讀出資料,fd[1]則隻能用于往管道裡寫入資料,而不能反過來使用,要實作雙向,就得使用兩個管道---都是阻塞的
//友善建立雙向管道
#include<sys/types>
#include<sys/socket.h>
int socketpair (int domain ,int type ,int protocol ,int fd[2]);
//dpmain隻能使用AF_UNIX,僅能在本地使用。最後一個參數則和pipe系統調用的參數一樣,隻不過socketpair建立的這對檔案描述符都是即可讀有可寫的,成功傳回0,失敗傳回-1并設定errno
//把标準輸入重定向到檔案或網絡
#include<unistd.h>
int dup (int file_descriptor);
int dup2 (int file_descriptor_one, int file_descriptor_two);
//分散讀和集中寫
#include<sys/uio.h>
ssize_t readv (int fd, const struct iovec* vector ,int count);
ssize_t writev (int fd , const struct iovec* vector, int count);
//vector中存儲的是iovec結構數組,count是vector數組的長度
//在兩個檔案描述符之間傳遞資料(完全在核心中操作),進而避免了核心緩沖區和使用者緩沖區之間的資料拷貝,效率很高,這被稱為--------零拷貝
#include<sys/sendfile.h>
ssize_t sendfile (int out_fd,int in_fd, off_t* offest ,size_t count);
//in_fd參數是待讀出内容的檔案描述符,out_fd是待寫入内容的檔案描述符,offest參數指定從讀入檔案流哪個位置開始讀,為空,則使用讀入檔案流預設的起始位置,count參數指定在檔案描述符之間傳輸的位元組數
//用于申請一段記憶體空間
#include<sys/mman.h>
void* mmap (void *start ,size_t length,int prot ,int flags ,int fd,off_t offest);
int munmap (void *start,size_t length);
//start允許使用者使用特定的位址作為起始位址,length指定記憶體段的長度,port參數用來設定記憶體段的通路權限
//PROT_READ 記憶體段可讀
//PROT_WRITE 記憶體段可寫
//PROT_EXEC 記憶體段可執行
//PROT_NONE 記憶體段不能被通路
mmap的flags
//用來在兩個檔案描述符之間移動資料----零拷貝
#include<fcntl.h>
ssize_t splice (int fd_in ,loff_t* off_in ,int fd_out , loff_t* off_out,size_t len, unsigned int flags);
//fd_int 如果是管道檔案描述符,則off_in設定NULL。如果不是,則off_in參數表示從輸入資料流的何處開始讀取資料,不為NULL則表示具體的偏移位置,fd_out和off_out同理,len參數指定移動資料的長度
splice的flags
//在兩個管道檔案描述符之間複制資料,也就是零拷貝操作
#include<fcntl.h>
ssize_t tee (int fd_in ,int fd_out ,size_t len ,unsigned int flags);
//參數與splice相同
//提供了對檔案描述符的各種控制操作
#include<fcntl.h>
int fcntl (int fd,int cmd,···);
//fd參數是被操作的檔案描述符,cmd參數指定執行何種操作,根據類型不同,可能還需要第三個可選參數arg
fcntl支援的操作1
fcntl支援的操作2
Part2七、Linux伺服器程式規範
伺服器程式規範:
- Linux伺服器程式一般以背景方式運作------守護程序
- Linux伺服器程式通常有一套日志系統,至少能輸出日志到檔案,有的進階伺服器還能輸出日志到專門的UDP伺服器,大部分背景程序都在 /var/log目錄下用使用者自己的日志目錄
- Linux伺服器程式一般以某個專門的非root身份運作
- Linux伺服器程式通常是可配置的,伺服器通常能處理很多指令行選項,如果一次運作的選項太多,則可以用配置檔案來管理,絕大多數伺服器程式都是有配置檔案的,并存放在/etc目錄下
- Linux伺服器程式程序通常會在啟動的時候生成一個PID檔案并存入/var/run目錄中記錄該背景程序的PID
- Linux伺服器程式通常需要考慮系統資源和限制,以預測自身能承受多大負荷
日志
Linux日志系統
#include<syslog.h>
void syslog (int priority ,const char* message , ...)
//priority參數是所謂的設施值與日志級别的按位或,預設值是LOG_USER
//日志級别
#include<syslog.h>
#define LOG_EMERG 0//系統不可用
#define LOG_ALERT 1//報警,需要了解立即動作
#define LOG_CRIT 2//非常嚴重的情況
#define LOG_ERR 3//錯誤
#define LOG_WARNING 4//警告
#define LOG_NOTICE 5//通知
#define LOG_INFO 6//資訊
#define LOG_DEBUG 7//調試
//改變syslog的預設輸出方式,進一步結構化日志内容
#include<syslog.h>
void openlog (const char* ident ,int logopt ,int facility) ;
//ident參數指定的字元串被添加到日志消息的日期和時間之後,通常被設定為程式的名字
//logopt參數對後續syslog調用行為配置
#define LOG_PID 0x01 //在日志消息中包含程式PID
#define LOG_CONS 0x02 //如果消息不能記錄到日志檔案,則列印至終端
#define LOG_ODELAY 0x04 //延遲打開日志功能知道第一次調用syslog
#define LOG_NDELAY 0x08 //不延遲打開日志功能
//設定syslog的日志掩碼
#include<syslog.h>
int setlogmask (int maskpri);
//maskpri參數指定日志掩碼值。該函數始終會成功,它傳回調用程序先前的日志掩碼值
//關閉日志功能
#include<syslog.h>
void closelog();
使用者資訊
//用來擷取和設定目前程序的真實使用者ID(UID)、有效使用者ID(EUID )、真實組ID(GID)和有效組ID(EGID)
#include<sys/types.h>
#include<unistd.h>
uid_t getuid(); //擷取真實使用者ID
uid_t geteuid(); //擷取有效使用者ID
gid_t getgid(); //擷取真實組ID
gid_t getegid(); //擷取有效組ID
int setuid(uid_t uid);//設定真實使用者ID
int seteuid(uid_t uid);//設定有效使用者ID
int setgid(gid_t gid);//設定真實組ID
int setegid (gid_t gid);//設定有效組ID
一個程序擁有兩個使用者ID:UID和EUID,EUID存在的目的是友善資源通路:它使得運作程式的使用者擁有該程式的有效使用者的權限
程序間關系
程序組
#include<unistd.h>
pid_t getgid (pid_t pid);
//成功傳回程序pid所屬的程序組的PGID,失敗傳回-1并設定errno
每個程序都有一個首領程序,其PGID和PID相同。程序将一直存在,直到其他所有程序都退出,或者加入到其他程序組
會話
//建立一個會話
#include<unistd.h>
pid_t setsid (void);
// 1.調用程序成為會話的首領,此時該程序是新會話的唯一成員
// 2.建立一個程序組,其PGID就是調用程序的PID,調用程序成為該組的首領
// 3.調用程序将甩開終端(如果有的話)
//讀取SID
#include<unistd.h>
pid_t getsid (pid_t pid);
程序間關系
程序間關系
系統資源限制
//Linux上運作的程式都會受到資源限制的影響
#include<sys/resource.h>
int getrlimit (int resource , struct rlimit* rlim); //讀取資源
int setrlimit (int resource , const struct rlimit* rlim);//設定資源
//rlimit結構體
struct rlimit
{
rlim_t rlim_cur;//指定資源的軟限制
rlim_t rlim_max;//指定資源的硬限制
}
//rlim_t 是一個整數類型
資源限制類型
改變工作目錄和根目錄
#include<unistd.h>
char* getcwd (char* buf,size_t size); //擷取目前工作目錄
int chdir (const char* path);//切換path指定的目錄
//改變程序根目錄函數
#include<unistd.h>
int chroot (const char* path);
Part3八、高性能伺服器程式架構
I/O處理單元---四種I/O模型和兩種高效事件處理模式
伺服器模型
C/S模型
C_S模型
- 由于客戶連接配接請求是随機到達的異步事件,是以伺服器需要使用某種I/O模型來監聽這一事件 當監聽到連接配接請求後,伺服器就調用accept函數接受它,并配置設定一個邏輯單元為新的連接配接服務。
- 邏輯單元可以是新建立的子程序,子線程或者其他
- 伺服器給用戶端配置設定的邏輯單元是由fork系統調用建立的子程序。
- 邏輯單元讀取客戶請求,處理該請求,然後将處理結果傳回給用戶端。
- 用戶端接收到伺服器回報的結果之後,可以繼續向伺服器發送請求,也可以立即主動關閉連接配接 如果用戶端主動關閉連接配接,則伺服器執行被動關閉連接配接
- 伺服器同時監聽多個客戶請求是通過select系統調用實作的
TCP工作流程
C/S模型非常适合資源相對集中的場合,并且它實作也很簡單,但其缺點也很明顯,伺服器是中心,通路量過大時,可能所有客戶都會得到很慢的響應。
P2P模型
優點:資源能夠充分、自由地共享
缺點:當使用者之間傳輸的請求過多時,網絡負載将加重
主機之前很難互相發現,是以實際使用的P2P模型通常帶有一個專門的發現伺服器
p2p模型
伺服器程式設計架構
伺服器基本架構
子產品 | 單個伺服器程式 | 伺服器機群 |
I/O處理單元 | 處理客戶連接配接,讀寫網絡資料 | 作為接入伺服器,實作負載均衡 |
邏輯單元 | 業務程序或線程 | 邏輯伺服器 |
網絡存儲單元 | 本地資料庫,檔案或緩存 | 資料庫伺服器 |
請求隊列 | 各單元之間的通信方式 | 各伺服器之間的永久TCP連接配接 |
I/O處理單元子產品:等待并接受新的客戶連接配接,接收客戶資料,将伺服器響應資料傳回給用戶端
邏輯單元通常是一個程序或線程:它分析并處理客戶資料,然後将結果傳遞給I/O處理單元或者直接發送給用戶端
網絡存儲單元:可以說資料庫,緩存和檔案,甚至是一台獨立的伺服器
請求隊列:是各個單元之間的通信方式和抽象I/O處理單元接收到客戶請求時,需要以某種方式通知一個邏輯單元來處理請求,多個邏輯單元同時通路一個存儲單元時,也需要某種機制來協調處理競态條件。請求隊列通常被實作為池的一部分。對伺服器來說,請求隊列是各台伺服器之間預先建立的,靜态的、永久的TCP連接配接
I/O模型
I/O模型 | 讀寫操作和阻塞階段 |
阻塞I/O | 程式阻塞于讀寫函數 |
I/O複用 | 程式阻塞于I/O複用系統調用,但可同時監聽 多個I/O事件,對I/O本身的讀寫操作是非阻塞的 |
SIGIO信号 | 信号觸發讀寫就緒事件,使用者程式執行讀寫操作。程式沒有阻塞階段 |
異步I/O | 核心執行讀寫操作并觸發讀寫完成事件,程式沒有阻塞階段 |
阻塞式IO
- 使用系統調用,并一直阻塞直到核心将資料準備好,之後再由核心緩沖區複制到使用者态,在等待核心準備的這段時間什麼也幹不了
- 下圖函數調用期間,一直被阻塞,直到資料準備好且從核心複制到使用者程式才傳回,這種IO模型為阻塞式IO
- 阻塞式IO是最流行的IO模型
程序阻塞于recvfrom
同步阻塞
優缺點
優點:開發簡單,容易入門;在阻塞等待期間,使用者線程挂起,在挂起期間不會占用CPU資源。
缺點:一個線程維護一個IO,不适合大并發,在并發量大的時候需要建立大量的線程來維護網絡連接配接,記憶體、線程開銷非常大。
非阻塞式IO
- 核心在沒有準備好資料的時候會傳回錯誤碼,而調用程式不會休眠,而是不斷輪詢詢問核心資料是否準備好
- 下圖函數調用時,如果資料沒有準備好,不像阻塞式IO那樣一直被阻塞,而是傳回一個錯誤碼。資料準備好時,函數成功傳回。
- 應用程式對這樣一個非阻塞描述符循環調用成為輪詢。
- 非阻塞式IO的輪詢會耗費大量cpu,通常在專門提供某一功能的系統中才會使用。通過為套接字的描述符屬性設定非阻塞式,可使用該功能
recvfrom調用
優缺點
同步非阻塞IO優點:每次發起IO調用,在核心等待資料的過程中可以立即傳回,使用者線程不會阻塞,實時性較好。
同步非阻塞IO缺點:多個線程不斷輪詢核心是否有資料,占用大量CPU時間,效率不高。一般Web伺服器不會采用此模式。
多路複用IO
- 類似與非阻塞,隻不過輪詢不是由使用者線程去執行,而是由核心去輪詢,核心監聽程式監聽到資料準備好後,調用核心函數複制資料到使用者态
- 下圖中select這個系統調用,充當代理類的角色,不斷輪詢注冊到它這裡的所有需要IO的檔案描述符,有結果時,把結果告訴被代理的recvfrom函數,它本尊再親自出馬去拿資料
- IO多路複用至少有兩次系統調用,如果隻有一個代理對象,性能上是不如前面的IO模型的,但是由于它可以同時監聽很多套接字,是以性能比前兩者高
多路複用包括:
- select:線性掃描所有監聽的檔案描述符,不管他們是不是活躍的。有最大數量限制(32位系統1024,64位系統2048)
- poll:同select,不過資料結構不同,需要配置設定一個pollfd結構數組,維護在核心中。它沒有大小限制,不過需要很多複制操作
- epoll:用于代替poll和select,沒有大小限制。使用一個檔案描述符管理多個檔案描述符,使用紅黑樹存儲。同時用事件驅動代替了輪詢。epoll_ctl中注冊的檔案描述符在事件觸發的時候會通過回調機制激活該檔案描述符。epoll_wait便會收到通知。最後,epoll還采用了mmap虛拟記憶體映射技術減少使用者态和核心态資料傳輸的開銷
優缺點
IO多路複用優點:系統不必建立維護大量線程,隻使用一個線程,一個選擇器即可同時處理成千上萬個連接配接,大大減少了系統開銷。
IO多路複用缺點:本質上,select/epoll系統調用是阻塞式的,屬于同步IO,需要在讀寫事件就緒後,由系統調用進行阻塞的讀寫。
信号驅動式IO
- 使用信号,核心在資料準備就緒時通過信号來進行通知
- 首先開啟信号驅動io套接字,并使用sigaction系統調用來安裝信号處理程式,核心直接傳回,不會阻塞使用者态
- 資料準備好時,核心會發送SIGIO信号,收到信号後開始進行io操作
信号驅動
異步IO
- 異步IO依賴信号處理程式來進行通知
- 不過異步IO與前面IO模型不同的是:前面的都是資料準備階段的阻塞與非阻塞,異步IO模型通知的是IO操作已經完成,而不是資料準備完成
- 異步IO才是真正的非阻塞,主程序隻負責做自己的事情,等IO操作完成(資料成功從核心緩存區複制到應用程式緩沖區)時通過回調函數對資料進行處理
- unix中異步io函數以aio_或lio_打頭
異步IO優點:真正實作了異步非阻塞,吞吐量在這幾種模式中是最高的。
異步IO缺點:應用程式隻需要進行事件的注冊與接收,其餘工作都交給了作業系統核心,是以需要核心提供支援。在Linux系統中,異步IO在其2.6才引入,目前也還不是灰常完善,其底層實作仍使用epoll,與IO多路複用相同,是以在性能上沒有明顯占優
五種IO模型對比
- 前面四種IO模型的主要差別在第一階段,他們第二階段是一樣的:資料從核心緩沖區複制到調用者緩沖區期間都被阻塞住!
- 前面四種IO都是同步IO:IO操作導緻請求程序阻塞,直到IO操作完成
- 異步IO:IO操作不導緻請求程序阻塞
對比
以上I/O模型詳解部分來源于網絡
兩種高效的事件處理模式
兩種事件處理模式Reactor和Proactor分别對應同步I/O模型、異步I/O模型
Reactor模式
它要求主線程(I/O處理單元)隻負責監聽檔案描述上是否有事件發生,有的話就立即将該事件通知工作線程(邏輯單元)。除此之外,主線程不做任何其他實質性的工作。-----讀寫資料,接受新的連接配接,以及處理客戶請求均在工作線程完成
- 主線程epoll核心事件表中注冊socket上的讀就緒事件
- 主線程調用epoll_wait等待socket上有資料可讀
- 當socket上有資料可讀時,epoll_wait通知主線程。主線程則将socket可讀事件放入請求隊列
- 睡眠在請求隊列上的某個工作線程被喚醒,它從socket讀取資料,并處理用戶端請求,然後往epoll核心事件表中注冊該socket上的寫就緒事件
- 主線程調用epoll_wait等待socket可寫
- 當socket可寫時,epoll_wait通知主線程。主線程将socket可寫事件放入請求隊列
- 睡眠在請求隊列上的某個工作線程被喚醒,它 往socket上寫入伺服器處理客戶請求的結果
系統的學習網絡程式設計,這篇就夠了!(來收藏夾裡吃灰)
Proactor模式
Proactor模式将所有I/O操作都交給主線程和核心來處理,工作線程僅僅負責業務邏輯
- 主線程調用aio_read函數向核心注冊socket上的讀完成事件,并告訴核心使用者讀緩沖區的位置,以及讀操作完成時如何通知應用程式
- 主線程繼續處理其他邏輯
- 當socket上的資料被讀入使用者緩沖區後,核心将向應用程式發送一個信号,以通知應用程式資料已經可用
- 應用程式預先定義好的信号處理函數選擇一個工作線程來處理客戶請求。工作線程處理完客戶請求之後,調用aio_write函數向核心注冊socket上的寫完成事件,并告訴核心使用者寫緩沖區的位置,以及寫操作完成時如何通知應用程式
- 主線程繼續處理其他邏輯
- 當使用者緩沖區的資料被寫入socket之後,核心将向應用程式發送一個信号,以通知應用程式資料以及發送完畢
- 應用程式預先定義好的信号處理函數選擇一個工作線程來做善後處理,比如決定是否關閉socket
系統的學習網絡程式設計,這篇就夠了!(來收藏夾裡吃灰)
同步I/O模型模拟出Proactor
主線程執行資料讀寫操作,讀完成之後,主線程向工作線程通知這一“完成事件”。那麼從工作線程的角度來看,它們就直接獲得了資料讀寫的結果,接下來要做的隻是對讀寫的操作進行邏輯處理
- 主線程往epoll核心事件表中注冊socket上的讀就緒事件
- 主線程調用epoll_wait等待socket上有資料可讀
- 當socket上有資料可讀時,epoll_wait通知主線程。主線程從socket循環讀取資料,直到沒有更多資料可讀,然後将資料封裝成一個請求對象并插入請求隊列
- 睡眠在請求隊列上的某個工作線程被喚醒,它獲得請求對象并處理客戶請求,然後往epoll核心事件表中注冊socket上的寫就緒事件
- 主線程調用epoll_wait等待socket可寫
- 當socket可寫時,epoll_wait通知主線程。主線程往socket上寫入伺服器處理客戶請求的結果
系統的學習網絡程式設計,這篇就夠了!(來收藏夾裡吃灰)
兩種高效的并發模型
并發模型是指I/O處理單元和多個邏輯單元之間協調完成任務的方法。兩種并發程式設計模式-------半同步/半異步模式、上司者/追随者模式
半同步/半異步模式
此同步和異步和前面I/O模型中的同步和異步完全不同。
在I/O模型中,“同步”和“異步”區分的是核心向應用程式通知的是何種I/O事件(是就緒事件還是完成事件),以及該由誰來完成I/O讀寫(應用程式還是核心)
在并發模式中,“同步”指的是程式完成按照代碼序列的順序執行:“異步”指的是程式的執行需要由系統事件來驅動
并發中的同步和異步
半同步/半異步工作流程
半同步_半異步工作流程
半同步/半異步模式變體------半同步/半異步反應堆
半同步_半異步反應堆模式
異步線程隻有一個,由主線程來充當,它負責監聽所有socket上的事件。如果監聽socket上有可讀事件發生------有新的連接配接請求到來,主線程就接受之以得到新的連接配接socket,然後往epoll核心事件表中注冊該socket上的讀寫事件。如果連接配接socket上有讀寫事件發生----有新的客戶請求到來或有資料要發送至用戶端,主線就将該連接配接socket插入請求隊列中。所有工作線程都睡眠在請求隊列上,當有任務到來時,它們将通過競争(比如申請互斥鎖)獲得任務的接管權。這種競争機制使得隻有空閑的工作線程才有機會來處理新任務,這是很合理的
缺點:
主線程和工作線程共享請求隊列。主線程往請求隊列中添加任務,或者工作線程從請求隊列中去除任務,都需要對請求隊列加鎖保護,進而白白耗費CPU時間。
每個工作線程都在同一時間隻能處理一個客戶請求。如果客戶數量較多,而工作線程較少,則請求隊列中将堆積很多任務對象,用戶端的響應速度将越來越慢。如果通過增加工作線程來解決這一問題,則工作線程的切換也将耗費大量CPU時間
變體----相對高效的
高效半同步_半異步模式
主線程隻管理監聽socket,連接配接socket由工作線程來管理。當有新的連接配接到來時,主線程就接受并将新傳回的連接配接socket派發給某個工作線程,此後該新socket上的任何I/O操作都由被選中的工作線程來處理,直到客戶關閉連接配接。主線程向工作線程派發socket的最簡單的方式,是往它和工作線程之間的管道裡寫資料。工作線程檢測到管道上有資料可讀時,就分析是否是一個新的客戶連接配接請求到來。如果是,則把該新socket上的讀寫事件注冊到自己的epool核心事件表中
上司者/追随者模式
上司者/追随者模式是多個工作線程輪流獲得事件源集合、輪流監聽、分發并處理事件的一種模式。在任意時間點,程式僅有一個上司者線程,它負責監聽I/O事件。而其他線程則都是追随者,它們休眠線上程池中等待成為新的上司者。目前的上司者如果檢測到I/O事件,首先要從線程池中推選出新的上司者線程,然後處理I/O事件。此時,新的上司者等待新的I/O事件,而原來的上司者則處理I/O事件,二者實作并發
包含:
句柄集、線程集、事件處理器和具體的事件處理器
上司者追随者模式元件
使用wait_for_event方法來監聽這些句柄上的I/O事件,并将其中的就緒事件通知給上司者線程
線程集中的線程在任一時間必處于以下三種狀态之一:
Leader:線程目前處于上司者身份,負責等待句柄集上的I/O事件Processing:線程正在處理事件。上司者檢測到I/O事件之後,可以轉移到processing狀态來處理該事件,并調用promote_new_leader方法推選出新的上司者:也可以指定其他追随者來處理事件,此時上司者的地位不變。當處于processing狀态的線程處理完事件之後,如果目前線程集中沒有上司者,則它将成為新的上司者,否則它就直接轉變為追随者Follower:線程目前處于追随者身份,通過調用線程集dejoin方法等待成為新的上司者,也可能被目前的上司者指定來處理新的任務
上司者追随者狀态轉移
事件處理器和具體的事件處理器
上司者追随者工作流程
在邏輯單元内部的一種高效程式設計方法--------有限狀态機
其他提高伺服器性能的手段
記憶體池、程序池、線程池和連接配接池避免不必要的拷貝,如使用共享記憶體、零拷貝盡量避免上下文的切換(線程切換)和鎖的使用,因為都會增加開銷
多程序程式設計
fork系統調用
用來Linux下建立新程序的系統
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
//該函數的每次調用都傳回兩次,在父程序中傳回的是子程序的PID,在子程序中則傳回0.該傳回值是後續代碼判斷目前程序是父程序還是子程序的依據。fork調用失敗時傳回-1,并設定errno。
fork函數複制目前程序,在核心程序表中建立一個新的程序表項。新的程序表項有很多屬性和原程序相同,比如堆指針、棧指針和标志寄存器的值。但也有許多屬性被賦予了新的值,比如該程序的PPID被設定成原程序的PID,信号位圖被清楚(原程序設定的信号處理函數不再對新程序起作用)
子程序的代碼與父程序完全相同,同時它還會複制父程序的資料(堆資料、棧資料和靜态資料)。資料的複制采用的是所謂的寫時複制,即隻有在任一程序(父程序或子程序)對資料執行了寫操作時,複制才會發生(顯示缺頁中斷,然後作業系統給子程序配置設定記憶體并複制父程序的資料)。即便如此,如果我們在程式中配置設定了大量記憶體,那麼使用fork時也應該十分謹慎,避免沒必要的記憶體配置設定和資料複制。建立程序後,父程序中打開的檔案描述符預設在子程序中也是打開的,且檔案描述符的引用計數加1.父程序的使用者根目錄,目前工作目錄等變量的引用計數均會加1。
exec系列系統調用
#include<unistd.h>
extern char** environ;
int execl(const char* path,const char* argv,...);
int execlp(const char* file,const char* arg, ...);
int execle(const char* path,const char* arg, ... ,char* const envp[]);
int execv(const char* path,char* const argv[]);
int execvp(const char* file,char* const argv[]);
int execve(const char* path,char* const argv[],char* const envp[]);
//path參數指定可執行檔案的完整路徑,file參數可以接受檔案名,該檔案的具體位置則在環境變量PATH中搜尋。arg接受可變參數,argv則接受參數數組,它們都會被傳遞給新程式(path或file指定的程式)的main函數,envp參數用于設定新程式的環境變量。如果未設定它,則新程式将使用由全局變量environ指定的環境變量
//出錯時傳回-1,并設定errno。如果沒出錯,則源程式中exec調用之後的代碼都不會執行,因為此時源程式已經被exec的參數指定的程式完全替換(包括代碼和資料)
exec函數不會關閉原程式打開的檔案描述符,除非該檔案描述符被設定了類似SOCK_CLOEXEC的屬性
處理僵屍程序
對于多程序程式而言,父程序一般需要跟蹤子程序的退出狀态。是以,當子程序結束運作時,核心不會立即釋放該程序的程序表表項,以滿足父程序後續對該子程序退出資訊的查詢(如果父程序還在運作)。子程序結束運作之後,父程序讀取其退出狀态之前,我們稱該子程序繼續運作。此時子程序的PPID将被作業系統設定為1,即init程序。init程序接管了子程序,并等待它結束。父程序退出之後,子程序退出之前,該子程序處于僵屍态。
//僵屍态會占據核心資源,是以使用下列函數來等待子程序的結束,并擷取子程序的傳回資訊,進而避免了僵屍程序的産生,或者使子程序呢個的僵屍态立即結束
#include<sys/types.h>
#incldue<sys.wait.h>
pid_t wait(int* stat_loc);
//wait函數将阻塞程序,直到該程序的某個子程序結束運作為止,它傳回結束運作的子程序的PID,并将該子程序的退出狀态資訊存儲于stat_loc參數指向的記憶體中。sys/wait.h頭檔案中定義了幾個宏來幫助解釋子程序的退出狀态資訊
pid_t waitpid(pid_t pid,int* stat_loc,int options);
//waitpid函數隻等待由pid參數指定的子程序。如果pid取值為-1,那麼它就和wait函數相同,即等待任意一個子程序結束。stat_loc參數的含義和wait函數的stat_loc參數相同,options參數可以控制waitpid函數的行為
//WNOHANG waitpid調用将是非阻塞的,目标程序未結束立即傳回0,如果正常退出則傳回PID,失敗傳回-1,并設定errno
常在SIGCHLD信号中調用waitpid,并在循環中徹底結束一個子程序
管道
管道是父程序和子程序通信的常用手段。管道能在父、子程序間傳遞資料,利用的是fork調用之後兩個管道檔案描述符(fd[0]和fd[1])都保持打開。一堆這樣的檔案描述符隻能保證父子程序間一個方向的資料傳輸,複制程序必須有一個關閉fd[0],另一個關閉fd[1]----是以必須使用兩個管道。socket程式設計提供了一個雙全工管道的系統調用:socketpair。---------隻能用于有關聯的兩個程序(如父子程序)
System IPC
這三種用來無關聯的多個程序之間通信的方式: 信号、共享記憶體、消息隊列
信号量
當多個程序通路系統上的某個資源的時候,就需要考慮程序的同步問題,以確定任意時刻隻有一個程序可以擁有對資源的獨占式通路----我們稱對共享資源的通路的代碼為關鍵代碼即臨界區。
公衆号裡有我更多的原創文章,歡迎關注,支援原創!
掃碼關注我們
更多高品質原創文章等你來看!