協議族中協議眾多, 這本書只選取了IP和TCP協定- 對網路程式設計影響最直接
同樣七層是osi參考模型, 簡化後得到四層不同層次之間, 透過介面互相交流, 這樣方便了各層次的修改
應用層負責處理應用程式的邏輯
表示層定義了資料的格式及加密
會話層它定義瞭如何開始、控制和結束一個會話,包括對多個雙向訊息的控制和管理,以便在只完成連續訊息的一部分時可以通知應用,從而使表示層看到的資料是連續的
傳輸層為兩台主機的應用提供端對端(end to end)的通訊. 與網路層使用的下一跳不同, 他只關心起始和終止, 中轉過程交給下層處理. 此層存在兩大協定TCP協定與UDP協定TCP協定(Transmission Control Protocol 傳輸控制協定)
可靠的, 面向连接, 基于流的服务
超时重传
和数据确认
等確保數據正常送達.不可靠的, 无连接的, 基于数据报的服务
数据确认
和超时重传
的問題有自己的长度
網路層實現了封包的選路和轉送. 只有封包到不了目標位址, 就下一跳
(hop by hop), 選擇最近的. IP協定(Internet Protocol)以及ICMP協定(Internet Control Message Protocol)後者協議是IP協定的補充, 用來偵測網路連線1. 錯誤封包, 用來回應狀態2.查詢封包(ping程式就是使用的此訊息來判斷資訊是否送達)
資料鏈路層實現了網卡介面的網路驅動程式. 這裡驅動程式方便了廠商的下層修改, 只需要向上層提供規定的介面即可. 存在兩個協定ARP協定(Address Resolve Protocol, 位址解析協定) . 還有RARP( Reverse ~, 逆位址解析協定) . 由於網路層使用IP位址尋址機器, 但是資料鏈結層使用物理位址(通常為MAC位址), 之間的轉換涉及到ARP協定ARP欺騙, 可能與此有關,目前不去學習
封裝上層協定傳送到下層協定. 透過封裝實現, 層與層之間傳輸的時候, 加上自己的頭部資訊. 被TCP封裝的資料成為TCP报文段
被UDP封裝的資料成為UDP数据报
再經IP封裝後成為IP数据报
最後經過資料鏈結層封裝後為帧
乙太網路最大資料幀1518位元組拋去14頭幀尾4校驗MTU: 訊框的最大傳輸單元一般為1500位元組MSS: TCP資料包最大的資料載量1460位元組= 1500位元組- 20Ip頭-20TCP頭還有額外的40位元組可選部分
ARP ARP協定能實現任意網路層位址到任意實體位址的轉換
IP協定是TCP/IP協定簇的核心協定, 是socket網路程式設計的基礎之一IP協定為上層協定提供無狀態, 無連線, 不可靠的服務
IP資料封包最大長度是65535(2^16 - 1)位元組, 但是有MTU的限制
當IP資料報的長度超過MTU 將會被分片傳輸. 分片可能發生在發送端, 也可能發生在中轉路由器, 還可能被多次分片. 只有在最終的目標機器上, 這些分片才會被核心中的ip模組重新組裝
路由機制
給定了目標IP位址後, 將會符合路由表中的哪一項呢? 分三個步驟
Tcp讀寫都是針對緩衝區來說, 所以沒有固定的讀寫次數對應關係.
UDP沒有緩衝區, 必須及時接受資料否則會丟包, 或接收緩衝區過小就會造成資料封包截斷
ISN-初始序號值32位元組後續的TCP封包中序號值seq = ISN + 封包段首字節在整個位元組流中的偏移32位元確認號收到的TCP封包序號值+1 . 這個32位確認號每次發送的是上次的應答
ACK標誌: 表示確認號碼是否有效. 攜帶ACK標誌的報文段稱為确认报文段
PSH標誌: 提示接收端應用程式從TCP接受緩衝區中讀走資料, 為後續資料騰出空間RST標誌: 要求對方重新建立連線攜帶......复位报文段
SYN標誌: 標誌請求建立一個連線攜帶......同步报文段
FIN標誌: 通知對方本端連線要關閉了, 攜帶..结束报文段
16位元視窗大小: 視窗指的是接收通告視窗, 告訴對方本端的TCP 接收緩衝區還能容納多少位元組的資料16位元校驗和:可靠传输的重要保障
發送端填充, 接收端執行CRC演算法校驗是否損壞, 同時校驗TCP头部
和数据部分
TCP連線的建立和關閉
# 三次握手
# 客户端发送请求连接 ISN= seq + 0 = 3683340920
# mss 最大数据载量1460
IP 192 . 168 . 80 . 1 . 7467 > ubuntu. 8000 :
Flags [S], seq 3683340920 , win 64240 ,
options [mss 1460 , nop ,wscale 8 , nop , nop ,sackOK], length 0
# 同意客户端连接
# ack = 客户端发送 seq + 1
# 同时发送服务端的seq
IP ubuntu. 8000 > 192 . 168 . 80 . 1 . 7467 :
Flags [S.], seq 938535101 , ack 3683340921 , win 64240 ,
options [mss 1460 , nop , nop ,sackOK, nop ,wscale 7 ], length 0
# 虽然这个报文段没有字节 但由于是同步报文段 需要占用一个序号值
# 这里是tcpdump的处理 ack显示相对值 即 3683340921 - 3683340920 = 1
IP 192 . 168 . 80 . 1 . 7467 > ubuntu. 8000 :
Flags [.], ack 938535102 , win 4106 , length 0
# 包含FIN标志 说明要求结束连接 也需要占用一个序号值
IP 192 . 168 . 80 . 1 . 7467 > ubuntu. 8000 :
Flags [F.], seq 1 , ack 1 , win 4106 , length 0
# 服务端确认关闭连接
IP ubuntu. 8000 > 192 . 168 . 80 . 1 . 7467 :
Flags [.], ack 2 , win 502 , length 0
# 服务端发送关闭连接
IP ubuntu. 8000 > 192 . 168 . 80 . 1 . 7467 :
Flags [F.], seq 1 , ack 2 , win 4105 , length 0
# 客户端确认
IP 192 . 168 . 80 . 1 . 7467 > ubuntu. 8000 :
Flags [.], ack 2 , win 503 , length 0
socket基礎api位於sys/socket.h
頭檔中socket最開始的意思是一個IP位址和埠對. 唯一的表示了TCP通訊的一段網路資訊api netdb.h
頭檔中
位元組序分為大端字节序
和小端字节序
由於大多數PC採用小端字節序(高位存在高位址處), 所以小端字節序又稱為主機字節序
為了防止不同機器字節序不同導致的錯亂問題. 規定傳輸的時候統一為大端字節序(網絡字節序). 這樣主機會根據自己的情況決定- 是否轉換接收到的數據的字節序
基礎連接
// 主机序和网络字节序转换
# include < netinet/in.h >
unsigned long int htonl ( unsigned long int hostlong); // host to network long
unsigned short int htons ( unsigned short int hostlong); // host to network short
unsigned long int htonl ( unsigned long int netlong);
unsigned short int htons ( unsigned short int netlong);
// IP地址转换函数
# include < arpa/inet.h >
// 将点分十进制字符串的IPv4地址, 转换为网络字节序整数表示的IPv4地址. 失败返回INADDR_NONE
in_addr_t inet_addr ( const char * strptr);
// 功能相同不过转换结果存在 inp指向的结构体中. 成功返回1 反之返回0
int inet_aton ( const char * cp, struct in_addr * inp);
// 函数返回一个静态变量地址值, 所以多次调用会导致覆盖
char * inet_ntoa ( struct in_addr in);
// src为 点分十进制字符串的IPv4地址 或 十六进制字符串表示的IPv6地址 存入dst的内存中 af指定地址族
// 可以为 AF_INET AF_INET6 成功返回1 失败返回-1
int inet_pton ( int af, const char * src, void * dst);
// 协议名, 需要转换的ip, 存储地址, 长度(有两个常量 INET_ADDRSTRLEN, INET6_ADDRSTRLEN)
const char * inet_ntop ( int af, const void * src, char * dst, socklen_t cnt);
// 创建 命名 监听 socket
# include < sys/types.h >
# include < sys/socket.h >
// domain指定使用那个协议族 PF_INET PF_INET6
// type指定服务类型 SOCK_STREAM (TCP协议) SOCK_DGRAM(UDP协议)
// protocol设置为默认的0
// 成功返回socket文件描述符(linux一切皆文件), 失败返回-1
int socket ( int domain, int type, int protocol);
// socket为socket文件描述符
// my_addr 为地址信息
// addrlen为socket地址长度
// 成功返回0 失败返回 -1
int bind ( int socket, const struct sockaddr * my_addr, socklen_t addrlen);
// backlog表示队列最大的长度
int listen ( int socket, int backlog);
// 接受连接 失败返回-1 成功时返回socket
int accept ( int sockfd, struct sockaddr * addr, socklen_t * addrlen)
客戶端
// 发起连接
#include <sys/types.h>
#include <sys/socket.h>
// 第三个参数为 地址指定的长度
// 成功返回0 失败返回-1
int connect ( int sockfd , const struct sockaddr * serv_addr , socklen_t addrlen );
// 关闭连接
#include <unistd.h>
// 参数为保存的socket
// 并非立即关闭, 将socket的引用计数-1, 当fd的引用计数为0, 才能关闭(需要查阅)
int close ( int fd );
// 立即关闭
#include <sys/socket.h>
// 第二个参数为可选值
// SHUT_RD 关闭读, socket的接收缓冲区的数据全部丢弃
// SHUT_WR 关闭写 socket的发送缓冲区全部在关闭前发送出去
// SHUT_RDWR 同时关闭读和写
// 成功返回0 失败为-1 设置errno
int shutdown ( int sockfd , int howto )
基礎TCP
#include <sys/socket.h>
#include <sys/types.h>
// 读取sockfd的数据
// buf 指定读缓冲区的位置
// len 指定读缓冲区的大小
// flags 参数较多
// 成功的时候返回读取到的长度, 可能小于预期长度, 需要多次读取. 读取到0 通信对方已经关闭连接, 错误返回-1
ssize_t recv ( int sockfd , void * buf , size_t len , int flags );
// 发送
ssize_t send ( int sockfd , const void * buf , size_t len , int flags );
選項名 | 意義 | 可用於發送 | 可用於接收 |
---|---|---|---|
MSG_CONFIRM | 指示鏈路層協定持續監聽, 直到得到答案.(只能用於SOCK_DGRAM和SOCK_RAW類型的socket) | Y | N |
MSG_DONTROUTE | 不查看路由表, 直接將資料傳送給本地的區域網路的主機(代表發送者知道目標主機就在本地網路中) | Y | N |
MSG_DONTWAIT | 非阻塞 | Y | Y |
MSG_MORE | 告知內核有更多的資料要發送, 等到資料寫入緩衝區完畢後,一併發送.減少短小的報文提高傳輸效率 | Y | N |
MSG_WAITALL | 讀取操作一直等待到讀取到指定位元組後才會返回 | N | Y |
MSG_PEEK | 看一下內快取數據, 並不會影響數據 | N | Y |
MSG_OOB | 發送或接收緊急數據 | Y | Y |
MSG_NOSIGNAL | 向讀取關閉的管道或socket連線中寫入資料不會觸發SIGPIPE訊號 | Y | N |
基礎UDP
#include <sys/types.h>
#include <sys/socket.h>
// 由于UDP不保存状态, 每次发送数据都需要 加入目标地址.
// 不过recvfrom和sendto 也可以用于 面向STREAM的连接, 这样可以省略发送和接收端的socket地址
ssize_t recvfrom ( int sockfd , void * buf , size_t len , int flags , struct sockaddr * src_addr , socklen_t * addrlen );
ssize_t sendto ( int sockfd , const void * buf , size_t len , ing flags , const struct sockaddr * dest_addr , socklen_t addrlen );
通用讀寫函數
#inclued <sys/socket.h>
ssize_t recvmsg ( int sockfd , struct msghdr * msg , int flags );
ssize_t sendmsg ( int sockfd , struct msghdr * msg , int flags );
struct msghdr
{
/* socket address --- 指向socket地址结构变量, 对于TCP连接需要设置为NULL*/
void * msg_name ;
socklen_t msg_namelen ;
/* 分散的内存块 --- 对于 recvmsg来说数据被读取后将存放在这里的块内存中, 内存的位置和长度由
* msg_iov指向的数组指定, 称为分散读(scatter read) ---对于sendmsg而言, msg_iovlen块的分散内存中
* 的数据将一并发送称为集中写(gather write);
*/
struct iovec * msg_iov ;
int msg_iovlen ; /* 分散内存块的数量*/
void * msg_control ; /* 指向辅助数据的起始位置*/
socklen_t msg_controllen ; /* 辅助数据的大小*/
int msg_flags ; /* 复制函数的flags参数, 并在调用过程中更新*/
};
struct iovec
{
void * iov_base /* 内存起始地址*/
size_t iov_len /* 这块内存长度*/
}
其他Api
#include <sys/socket.h>
// 用于判断 sockfd是否处于带外标记, 即下一个被读取到的数据是否是带外数据,
// 是的话返回1, 不是返回0
// 这样就可以选择带MSG_OOB标志的recv调用来接收带外数据.
int sockatmark ( int sockfd );
// getsockname 获取sockfd对应的本端socket地址, 存入address指定的内存中, 长度存入address_len中 成功返回0失败返回-1
// getpeername 获取远端的信息, 同上
int getsockname ( int sockfd , struct sockaddr * address , socklen_t * address_len );
int getpeername ( int sockfd , struct sockaddr * address , socklen_t * address_len );
/* 以下函数头文件均相同*/
// sockfd 目标socket, level执行操作协议(IPv4, IPv6, TCP) option_name 参数指定了选项的名字. 后面值和长度
// 成功时返回0 失败返回-1
int getsockopt ( int sockfd , int level , int option_name , void * option_value ,
socklen_t restrict option_len );
int setsockopt ( int sockfd , int level , int option_name , void * option_value ,
socklen_t restrict option_len );
SO_REUSEADDR | 重複使用本地地址 | sock被設定此屬性後, 即使sock在被bind()後處於TIME_WAIT狀態, 此時與他綁定的socket地址依然能夠立即重用來綁定新的sock |
---|---|---|
SO_RCVBUF | TCP接收緩衝區大小 | 最小值為256位元組. 設定完後系統會自動加倍你所設定的值. 多出來的一倍將用用作空閒緩衝區處理擁塞 |
SO_SNDBUF | TCP發送緩衝區大小 | 最小值為2048字節 |
SO_RCVLOWAT | 接收的低水位標記 | 預設為1位元組, 當TCP接收緩衝區中可讀資料的總數大於其低水位標記時, IO復用系統呼叫將通知應用程式可以從對應的socket上讀取數據 |
SO_SNDLOWAT | 發送的高水位標記 | 預設為1位元組, 當TCP發送緩衝區中空閒空間大於低水位標記的時候可以寫入數據 |
SO_LINGER |
struct linger
{
int l_onoff /* 开启非0, 关闭为0*/
int l_linger ; /* 滞留时间*/
/*
* 当onoff为0的时候此项不起作用, close调用默认行为关闭socket
* 当onoff不为0 且linger为0, close将立即返回, TCP将丢弃发送缓冲区的残留数据, 同时发送一个复位报文段
* 当onoff不为0 且linger大于0 . 当socket阻塞的时候close将会等待TCP模块发送完残留数据并得到确认后关
* 闭, 如果是处于非阻塞则立即关闭
*/
};
網路資訊API
#include <netdb.h>
// 通过主机名查找ip
struct hostent * gethostbyname ( const char * name );
// 通过ip获取主机完整信息
// type为IP地址类型 AF_INET和AF_INET6
struct hostent * gethostbyaddr ( const void * addr , size_t len , int type );
struct hostent
{
char * h_name ; /* Official name of host. */
char * * h_aliases ; /* Alias list. */
int h_addrtype ; /* Host address type. */
int h_length ; /* Length of address. */
char * * h_addr_list ; /* List of addresses from name server. */
}
int main ( int argc , char * argv [])
{
if ( argc != 2 )
{
printf ( "非法输入n" );
exit ( 0 );
}
char * name = argv [ 1 ];
struct hostent * hostptr {};
hostptr = gethostbyname ( name );
if ( hostptr == nullptr )
{
printf ( "输入存在错误 或无法获取n" );
exit ( 0 );
}
printf ( "Official name of hostptr: %sn" , hostptr -> h_name );
char * * pptr ;
char inet_addr [ INET_ADDRSTRLEN ];
printf ( "Alias list:n" );
for ( pptr = hostptr -> h_aliases ; * pptr != nullptr ; ++ pptr )
{
printf ( "t%sn" , * pptr );
}
switch ( hostptr -> h_addrtype )
{
case AF_INET :
{
printf ( "List of addresses from name server:n" );
for ( pptr = hostptr -> h_addr_list ; * pptr != nullptr ; ++ pptr )
{
printf ( "t%sn" ,
inet_ntop ( hostptr -> h_addrtype , * pptr , inet_addr , sizeof ( inet_addr )));
}
break ;
}
default :
{
printf ( "unknow address typen" );
exit ( 0 );
}
}
return 0 ;
}
/*
./run baidu.com
Official name of hostptr: baidu.com
Alias list:
List of addresses from name server:
39.156.69.79
220.181.38.148
*/
以下兩個函數透過讀取/etc/services檔案來取得服務資訊以下內容來自維基百科
Service檔案是現代作業系統在etc目錄下的一個設定文件,記錄網路服務名稱對應的連接埠號碼與協定其用途如下
#include <netdb.h>
// 根据名称获取某个服务的完整信息
struct servent getservbyname ( const char * name , const char * proto );
// 根据端口号获取服务信息
struct servent getservbyport ( int port , const char * proto );
struct servent
{
char * s_name ; /* 服务名称*/
char * * s_aliases ; /* 服务的别名列表*/
int s_port ; /* 端口号*/
char * s_proto ; /* 服务类型, 通常为TCP或UDP*/
}
#include <netdb.h>
// 内部使用的gethostbyname 和 getserverbyname
// hostname 用于接收主机名, 也可以用来接收字符串表示的IP地址(点分十进制, 十六进制字符串)
// service 用于接收服务名, 字符串表示的十进制端口号
// hints参数 对getaddrinfo的输出进行更准确的控制, 可以设置为NULL, 允许反馈各种有用的结果
// result 指向一个链表, 用于存储getaddrinfo的反馈结果
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 ;
char * ai_canonname ; /* 主机的别名*/
struct sockaddr * ai_addr ; /* 指向socket地址*/
struct addrinfo * ai_next ; /* 指向下一个结构体*/
}
// 需要手动的释放堆内存
void freeaddrinfo ( struct addrinfo * res );
#include <netdb.h>
// host 存储返回的主机名
// serv存储返回的服务名
int getnameinfo ( const struct sockaddr * sockaddr , socklen_t addrlen , char * host , socklen_t hostlen , char * serv
socklen_t servlen , int flags );
測試使用
telnet ip port #来连接服务器的此端口
netstat -nt | grep port #来查看此端口的监听
Linux提供的高級IO函數, 自然是特定條件下能力更強, 不然要他幹啥, 特定條件自然限制了他的使用頻率文件描述符檔案描述符在是一個非負整數。是一個索引值,指向核心為每一個行程所維護的該行程開啟檔案的記錄表。 STDOUT_FILENO(值為1)- 值為1的文件描述符為標準輸出, 關閉STDOUT_FILENO後用dup即可返回最小可用值(目前為, 1) 這樣輸出就重定向到了調用dup的參數指向的文件
pipe函數這個函數可用於建立一個管道, 實現進程間的通訊.
// 函数定义
// 参数文件描述符数组 fd[0] 读出 fd[1]写入 单向管道
// 成功返回0, 并将一对打开的文件描述符填入其参数指向的数组
// 失败返回-1 errno
#include <unistd.h>
int pipe ( int fd [ 2 ]);
// 双向管道
// 第一个参数为 协议PF_UNIX(书上是AF_UNIX)感觉这里指明协议使用PF更好一些
#include <sys/types.h>
#include <sys/socket.h>
int socketpair ( int domain , int type , int protocol , int fd [ 2 ]);
學習了後面的內容了解到了進程間通訊, 回來補上一個例子
int main ()
{
int fds [ 2 ];
socketpair ( PF_UNIX , SOCK_STREAM , 0 , fds );
int pid = fork ();
if ( pid == 0 )
{
close ( fds [ 0 ]);
char a [] = "123" ;
send ( fds [ 1 ], a , strlen ( a ), 0 );
}
else if ( pid > 0 )
{
close ( fds [ 1 ]);
char b [ 20 ] {};
recv ( fds [ 0 ], b , 20 , 0 );
printf ( "%s" , b );
}
}
dup和dup2函數複製一個現有的文件描述符
#include <unistd.h>
// 返回的文件描述符总是取系统当前可用的最小整数值
int dup ( int oldfd );
// 可以用newfd来制定新的文件描述符, 如果newfd已经被打开则先关闭
// 如果newfd==oldfd 则不关闭newfd直接返回
int dup2 ( int oldfd , int newfd );
dup函數建立一個新的檔案描述子, 新的檔案描述子和原有的file_descriptor共同指向相同的目標. 回來補上範例, 這個例子由於關掉了STDOUT_FILENO
dup最小的即為STDOUT_FILENO
所以標準輸出都到了這個文件之中
int main ()
{
int filefd = open ( "/home/lsmg/1.txt" , O_WRONLY );
close ( STDOUT_FILENO );
dup ( filefd );
printf ( "123n" );
exit ( 0 );
}
readv/writev
#include <sys/uio.h>
// count 为 vector的长度, 即为有多少块内存
// 成功时返回写入读取的长度 失败返回-1
ssize_t readv ( int fd , const struct iovec * vector , int count );
ssize_t writev ( int fd , const struct iovec * vector , int count );
struct iovec {
void * iov_base /* 内存起始地址*/
size_t iov_len /* 这块内存长度*/
}
回來補上一個使用例子, 這個例子將一個int的內存表示寫入到了文件之中使用hexdump查看這個文件0000000 86a0 0001
可以看到186a0
即為100000
// 2020年1月7日16:52:11
int main ()
{
int file = open ( "/home/lsmg/1.txt" , O_WRONLY );
int temp = 100000 ;
iovec temp_iovec {};
temp_iovec . iov_base = & temp ;
temp_iovec . iov_len = sizeof ( temp );
writev ( file , & temp_iovec , 1 );
}
sendfile函數
#include <sys/sendfile.h>
// offset为指定输入流从哪里开始读, 如果为NULL 则从开头读取
ssize_t sendfile ( int out_fd , int in_fd , off_t * offset , size_t count );
O_RDONLY只读模式
O_WRONLY只写模式
O_RDWR读写模式
int open ( file_name , flag );
stat結構體, 可用fstat產生,簡直就是檔案的身份證
#include <sys/stat.h>
struct stat
{
dev_t st_dev ; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino ; /* inode number -inode节点号*/
mode_t st_mode ; /* protection -保护模式?*/
nlink_t st_nlink ; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid ; /* user ID of owner -user id*/
gid_t st_gid ; /* group ID of owner - group id*/
dev_t st_rdev ; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size ; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize ; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks ; /* number of blocks allocated -文件所占块数*/
time_t st_atime ; /* time of last access -最近存取时间*/
time_t st_mtime ; /* time of last modification -最近修改时间*/
time_t st_ctime ; /* time of last status change - */
};
身分證產生函數
// 第一个参数需要调用open生成文件描述符
// 下面其他两个为文件全路径
int fstat ( int filedes , struct stat * buf );
// 当路径指向为符号链接的时候, lstat为符号链接的信息. stat为符号链接指向文件信息
int stat ( const char * path , struct stat * buf );
int lstat ( const char * path , struct stat * buf );
/*
* ln -s source dist 建立软连接, 类似快捷方式, 也叫符号链接
* ln source dist 建立硬链接, 同一个文件使用多个不同的别名, 指向同一个文件数据块, 只要硬链接不被完全
* 删除就可以正常访问
* 文件数据块 - 文件的真正数据是一个文件数据块, 打开的`文件`指向这个数据块, 就是说
* `文件`本身就类似快捷方式, 指向文件存在的区域.
*/
mmap和munmap函數
mmap
建立一塊進程通訊共享的記憶體(可以將檔案映射入其中), munmap
釋放這塊內存
#include <sys/mman.h>
// start 内存起始位置, 如果为NULL则系统分配一个地址 length为长度
// port参数 PROT_READ(可读) PROT_WRITE(可写) PROT_EXEC(可执行), PROT_NONE(不可访问)
// flag参数 内存被修改后的行为
// - MAP_SHARED 进程间共享内存, 对内存的修改反映到映射文件中
// - MAP_PRIVATE 为调用进程私有, 对该内存段的修改不会反映到文件中
// - MAP_ANONUMOUS 不是从文件映射而来, 内容被初始化为0, 最后两个参数被忽略
// 成功返回区域指针, 失败返回 -1
void * mmap ( void * start , size_t length , int port , int flags , int fd , off_t offset );
// 成功返回0 失败返回-1
int munmap ( void * start , size_t length );
splice函數用於在兩個檔案名稱描述符之間移動資料, 0拷貝操作
#include <fcntl.h>
// fd_in 为文件描述符, 如果为管道文件描述符则 off_in必须为NULL, 否则为读取开始偏移位置
// len为指定移动的数据长度, flags参数控制数据如何移动.
// - SPLICE_F_NONBLOCK 非阻塞splice操作, 但会受文件描述符自身的阻塞
// - SPLICE_F_MORE 给内核一个提示, 后续的splice调用将读取更多的数据???????
ssize_t splice ( int fd_in , loff_t * off_in , int fd_out , loff_t * off_out , size_t len , unsigned int flags );
// 使用splice函数 实现echo服务器
int main ( int argc , char * argv [])
{
if ( argc <= 2 )
{
printf ( "the parmerters is wrongn" );
exit ( errno );
}
char * ip = argv [ 1 ];
int port = atoi ( argv [ 2 ]);
printf ( "the port is %d the ip is %sn" , port , ip );
int sockfd = socket ( PF_INET , SOCK_STREAM , 0 );
assert ( sockfd >= 0 );
struct sockaddr_in address {};
address . sin_family = AF_INET ;
address . sin_port = htons ( port );
inet_pton ( AF_INET , ip , & address . sin_addr );
int ret = bind ( sockfd , ( sockaddr * ) & address , sizeof ( address ));
assert ( ret != -1 );
ret = listen ( sockfd , 5 );
int clientfd {};
sockaddr_in client_address {};
socklen_t client_addrlen = sizeof ( client_address );
clientfd = accept ( sockfd , ( sockaddr * ) & client_address , & client_addrlen );
if ( clientfd < 0 )
{
printf ( "accept errorn" );
}
else
{
printf ( "a new connection from %s:%d successn" , inet_ntoa ( client_address . sin_addr ), ntohs ( client_address . sin_port ));
int fds [ 2 ];
pipe ( fds );
ret = splice ( clientfd , nullptr , fds [ 1 ], nullptr , 32768 , SPLICE_F_MORE );
assert ( ret != -1 );
ret = splice ( fds [ 0 ], nullptr , clientfd , nullptr , 32768 , SPLICE_F_MORE );
assert ( ret != -1 );
close ( clientfd );
}
close ( sockfd );
exit ( 0 );
}
select 函數select函數在第二個參數列表可讀的時候返回或是等到了規定的時間返回
傳回之後第二個參數指向fdset的集合被修改為可讀的fd列表這就需要每次回傳後更新fdset集合
返回後此函數的返回值為可讀的fd數量, 遍歷fdset集合同時使用FD_ISSET判斷fdset[i] 是否在其中然後判斷此fd是否為listenfd 如果是則接受新的連接如果不是說明是已經接受的其他fd 判斷是有資料可讀還是此連線斷開
#include <fcntl.h>
// maxfdp 最大数 FD_SETSIZE
// struct fd_set 一个集合,可以存储多个文件描述符
// - FD_ZERO(&fd_set) 清空 -FD_SET(fd, &fd_set) 放入fd FD_CLR(fd, &fd_set)从其中清除fd
// - FD_ISSET(fd, &fd_set) 判断是否在其中
// readfds 需要监视的文件描述符读变化, 其中的文件描述符可读的时候返回
// writefds 需要监视的文件描述符写变化, 其中的文件描述符可写的时候返回
// errorfds 错误
// timeout 传入NULL为阻塞, 设置为0秒0微秒则变为非阻塞函数
// 返回值 负值为错误 等待超时说明文件无变化返回0 有变化返回正值
int select ( int maxfdp , fd_set * readfds , fd_set * writefds , fd_set * errorfds , struct timeval * timeout );
#define exit_if ( r , ...)
{
if (r)
{
printf(__VA_ARGS__);
printf("errno no: %d, error msg is %s", errno, strerror(errno));
exit(1);
}
}
int main ( int argc , char * argv [])
{
int keyboard_fd = open ( "/dev/tty" , O_RDONLY | O_NONBLOCK );
exit_if ( keyboard_fd < 0 , "open keyboard fd errorn" );
fd_set readfd ;
char recv_buffer = 0 ;
while (true)
{
FD_ZERO ( & readfd );
FD_SET ( 0 , & readfd );
timeval timeout { 5 , 0 };
int ret = select ( keyboard_fd + 1 , & readfd , nullptr , nullptr , & timeout );
exit_if ( ret == -1 , "select errorn" );
if ( ret > 0 )
{
if ( FD_ISSET ( keyboard_fd , & readfd ))
{
recv_buffer = 0 ;
read ( keyboard_fd , & recv_buffer , 1 );
if ( 'n' == recv_buffer )
{
continue ;
}
if ( 'q' == recv_buffer )
{
break ;
}
printf ( "the input is %cn" , recv_buffer );
}
}
if ( ret == 0 )
{
printf ( "timeoutn" );
}
}
}
sudo service rsyslog restart // 启动守护进程
#include <syslog.h>
// priority参数是所谓的设施值(记录日志信息来源, 默认为LOG_USER)与日志级别的按位或
// - 0 LOG_EMERG /* 系统不可用*/
// - 1 LOG_ALERT /* 报警需要立即采取行动*/
// - 2 LOG_CRIT /* 非常严重的情况*/
// - 3 LOG_ERR /* 错误*/
// - 4 LOG_WARNING /* 警告*/
// - 5 LOG_NOTICE /* 通知*/
// - 6 LOG_INFO /* 信息*/
// -7 LOG_DEBUG /* 调试*/
void syslog ( int priority , const char * message , .....);
// ident 位于日志的时间后 通常为名字
// logopt 对后续 syslog调用的行为进行配置
// - 0x01 LOG_PID /* 在日志信息中包含程序PID*/
// - 0x02 LOG_CONS /* 如果信息不能记录到日志文件, 则打印到终端*/
// - 0x04 LOG_ODELAY /* 延迟打开日志功能直到第一次调用syslog*/
// - 0x08 LOG_NDELAY /* 不延迟打开日志功能*/
// facility参数可以修改syslog函数中的默认设施值
void openlog ( const char * ident , int logopt , int facility );
// maskpri 一共八位 0000-0000
// 如果将最后一个0置为1 表示 记录0级别的日志
// 如果将最后两个0都置为1 表示记录0和1级别的日志
// 可以通过LOG_MASK() 宏设定 比如LOG_MASK(LOG_CRIT) 表示将倒数第三个0置为1, 表示只记录LOG_CRIT
// 如果直接设置setlogmask(3); 3的二进制最后两个数均为1 则记录 0和1级别的日志
int setlogmask ( int maskpri );
// 关闭日志功能
void closelog ();
UID - 真實使用者ID EUID - 有效使用者ID - 方便資源存取GID - 真實群組ID EGID - 有效群組ID
#include <sys/types.h>
#include <unistd.h>
uid_t getuid ();
uid_t geteuid ();
gid_t getgid ();
gid_t getegid ();
int setuid ( uid_t uid );
int seteuid ( uid_t euid );
int setgid ( gid_t gid );
int setegid ( gid_t gid );
可以透過setuid
和setgid
切換用戶root用戶uid和gid均為0
PGID - 進程組ID(Linux下每個進程隸屬於一個進程組)
#include <unistd.h> pid_t getpgid(pid_t pid); 成功時回傳pid所屬的pgid 失敗回傳-1 int setpgid(pid_t pid, pid_t pgid);
會話一些有關聯的進程組將形成一個會話略過
查看進程關係ps和less
資源限制略改變目錄略
伺服器模型-CS模型
優點
模式圖
編寫的demo 沒有用到fork函數. 後續待完善
伺服器框架IO模型
這個模型大概能夠理解, 自己也算是學了半年的Javaweb.
socket在創建的時候默認是阻塞的, 不過可以通過傳SOCK_NONBLOCK
參解決非阻塞調用都會立即返回但可能事件沒有發生(recv沒有接收到信息), 沒有發生和出錯都會返回-1
所以需要通過errno
來區分這些錯誤.事件未發生accept, send,recv errno被設定為EAGAIN(再来一次)
或EWOULDBLOCK(期望阻塞)
connect被設定為EINPROGRESS(正在处理中)
需要在事件已經發生的情況下去呼叫非阻塞IO, 才能提高效能
常用IO復用函數select
poll
epoll_wait
將在第九章後面說明訊號將在第十章說明
兩種高效率的事件處理模式和並發模式
程式分為運算密集型(CPU使用很多, IO資源使用很少)和IO密集型(反過來). 前者使用並發程式設計反而會降低效率, 後者則會提升效率並發程式設計有多進程和多執行緒兩種方式
並發模式- IO單元和多個邏輯單元之間協調完成任務的方法. 伺服器主要有兩種並發模式
半同步/半非同步模式在IO模型中, 非同步和同步的區分是核心向應用程式通知的是何種IO事件(就緒事件還是完成事件), 以及由誰來完成IO讀寫(應用程式還是核心)
而在這裡(並發模式) 同步指的是完全按照代碼序列的順序執行- 按照同步方式運行的線程稱為同步線程異步需要係統事件(中斷, 信號)來驅動- 按照異步方式運行的線程稱為異步執行緒
伺服器(需要較好的即時性且能同時處理多個客戶請求) - 一般使用同步線程和異步線程來實現,即為半同步/半異步模式同步線程- 處理客戶邏輯, 處理請求隊列中的對象異步線程- 處理IO事件, 接收到客戶請求後將其封裝成請求對象並插入請求隊列
半同步/半非同步模式存在變體半同步/半反应堆模式
非同步線程- 主線程- 負責監聽所有socket上的事件
領導者/追隨者模式略
高效率程式設計方法- 有限狀態機
// 状态独立的有限状态机
STATE_MACHINE ( Package _pack ) {
PackageType _type = _pack . GetType ();
switch ( _type ) {
case type_A :
xxxx ;
break ;
case type_B :
xxxx ;
break ;
}
}
// 带状态转移的有限状态机
STATE_MACHINE () {
State cur_State = type_A ;
while ( cur_State != type_C ) {
Package _pack = getNewPackage ();
switch ( cur_State ) {
case type_A :
process_package_state_A ( _pack );
cur_State = type_B ;
break ;
case type_B :
xxxx ;
cur_State = type_C ;
break ;
}
}
}
花了小一個小時終於一個字母一個字母的抄完了那個5000多字的代碼@2019年9月8日22:08:46@
池- 用空間換取時間進程池和執行緒池
資料複製- 高效能的伺服器應該盡量避免不必要的複製
上下文切換和鎖減少锁
的作用區域. 不應該創建太多的工作進程, 而是使用專門的業務邏輯線程.
I/O復用使得程式能同時監聽多個檔案描述子.
常用手段select
, poll
, epoll
# include < sys/select.h >
// nfds - 被监听的文件描述符总数
// 后面三个分别指向 可读, 可写, 异常等事件对应的文件描述符集合
// timeval select超时时间 如果传递0 则为非阻塞, 设置为NULL则为阻塞
// 成功返回就绪(可读, 可写, 异常)文件描述符的总数, 没有则返回0 失败返回-1
int select ( int nfds, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout);
//操作fd_set的宏
FD_ZERO ( fd_set * fdset);
FD_SET ( int fd, fd_set * fdset);
FD_CLR ( int fd, fd_set * fdset);
FD_ISSET ( int fd, fd_set * fdset);
// 设置 timeval 超时时间
struct timeval
{
long tv_sec; // 秒
long tv_usec; // 微秒
}
select
檔案描述符就緒條件
poll
# include < poll.h >
// fds 结构体类型数组 指定我们感兴趣的文件描述符上发生的可读可写和异常事件
// nfds 遍历结合大小 左闭右开
// timeout 单位为毫秒 -1 为阻塞 0 为立即返回
int poll ( struct pollfd * fds, nfds_t nfds, int timeout);
struct pollfd
{
int fd;
short events; //注册的事件, 告知poll监听fd上的哪些事件
short revents; // 实际发生的事件
}
# define exit_if (r, ...)
{
if (r)
{
printf (__VA_ARGS__);
printf ( " errno no: %d, error msg is %s " , errno, strerror (errno));
exit ( 1 );
}
}
struct client_info
{
char *ip_;
int port_;
};
int main ( int argc, char * argv[])
{
int port = 8001 ;
char ip[] = " 127.0.0.1 " ;
struct sockaddr_in address;
address. sin_port = htons (port);
address. sin_family = AF_INET;
address. sin_addr . s_addr = htons (INADDR_ANY);
int listenfd = socket (PF_INET, SOCK_STREAM, 0 );
exit_if (listenfd < 0 , " socket error n " );
int ret = bind (listenfd, ( struct sockaddr *)&address, sizeof (address));
exit_if (ret == - 1 , " bind error n " );
ret = listen (listenfd, 5 );
exit_if (ret == - 1 , " listen error n " );
constexpr int MAX_CLIENTS = 1024 ;
struct pollfd polls[MAX_CLIENTS] = {};
struct client_info clientsinfo[MAX_CLIENTS] = {};
polls[ 3 ]. fd = listenfd;
polls[ 3 ]. events = POLLIN | POLLRDHUP;
while ( true )
{
ret = poll (polls, MAX_CLIENTS + 1 , - 1 );
exit_if (ret == - 1 , " poll error n " );
for ( int i = 3 ; i <= MAX_CLIENTS; ++i)
{
int fd = polls[i]. fd ;
if (polls[i]. revents & POLLRDHUP)
{
polls[i]. events = 0 ;
printf ( " close fd-%d from %s:%d n " , fd, clientsinfo[fd]. ip_ , clientsinfo[fd]. port_ );
}
if (polls[i]. revents & POLLIN)
{
if (fd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addresslen = sizeof (client_address);
int clientfd = accept (listenfd, ( struct sockaddr *)&client_address,
&client_addresslen);
struct client_info *clientinfo = &clientsinfo[clientfd];
clientinfo-> ip_ = inet_ntoa (client_address. sin_addr );
clientinfo-> port_ = ntohs (client_address. sin_port );
exit_if (clientfd < 0 , " accpet error, from %s:%d n " , clientinfo-> ip_ ,
clientinfo-> port_ );
printf ( " accept from %s:%d n " , clientinfo-> ip_ , clientinfo-> port_ );
polls[clientfd]. fd = clientfd;
polls[clientfd]. events = POLLIN | POLLRDHUP;
}
else
{
char buffer[ 1024 ];
memset (buffer, '