第一部分:基礎(chǔ)API
1、主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序
我們都知道字節(jié)序分位大端和小端:
- 大端是高位字節(jié)在低地址,低位字節(jié)在高地址
- 小端是順序字節(jié)存儲,高位字節(jié)在高地址,低位字節(jié)在低地址
既然機器存在字節(jié)序不一樣,那么網(wǎng)絡(luò)傳輸過程中必然涉及到發(fā)出去的數(shù)據(jù)流需要轉(zhuǎn)換,所以發(fā)送端會將數(shù)據(jù)轉(zhuǎn)換為大端模式發(fā)送,系統(tǒng)提供API實現(xiàn)主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序的轉(zhuǎn)換。
#include < netinet/in.h >
// 轉(zhuǎn)換長整型
unsigned long htonl(unsigned long int hostlong);
unsigned long ntohl(unsigned long int netlong);
// 轉(zhuǎn)換短整型
unsigned short htonl(unsigned short int hostshort);
unsigned short ntohl(unsigned short int netshort);
2、socket地址
(1)socket地址包含兩個部分,一個是什么協(xié)議,另一個是存儲數(shù)據(jù),如下:
struct sockaddr
{
sa_family_t sa_family; // 取值:PF_UNIX(UNIX本地協(xié)議簇),PF_INET(ipv4),PF_INET6(ipv6)
char sa_data[14]; // 根據(jù)上面的協(xié)議簇存儲數(shù)據(jù)(UNIX本地路徑,ipv4端口和IP,ipv6端口和IP)
};
(2)各個協(xié)議簇專門的結(jié)構(gòu)體
// unix本地協(xié)議簇
struct sockaddr_un
{
sa_family_t sin_family; // AF_UNIX
char sun_path[18];
};
// ipv4本地協(xié)議簇
struct sockaddr_in
{
sa_family_t sin_family; // AF_INET
u_int16_t sin_port;
struct in_addr sin_addr;
};
// ipv6本地協(xié)議簇
struct sockaddr_in6
{
sa_family_t sin_family; // AF_INET6
u_int16_t sin6_port;
u_int32_t sin6_flowinfo;
...
};
3、socket創(chuàng)建
socket,bind,listen,accept,connect,close和shutdown作為linux網(wǎng)絡(luò)開發(fā)必備知識, 大家應(yīng)該都都耳熟能詳了,所以我就簡單介紹使用方式,重點介紹參數(shù)注意事項。
#include < sys/types.h >
#include < sys/socket.h >
int socket(int domain, int type, int protocol);
(1)domain
參數(shù)目的是告訴底層協(xié)議簇,選項(PF_INET, PF_INET6和PF_UNIX);
(2)type
指定服務(wù)類型(流數(shù)據(jù)和數(shù)據(jù)報),選項(SOCK_STREAM和SOCK_UGRAM);
(3)protocol
默認0即可;
注意:
socket的屬性SOCK_NONBLOCK
和SOCK_CLOEXEC
,分別標識非阻塞和fork子進程在子進程中關(guān)閉socket;
4、bind
#include < sys/types.h >
#include < sys/socket.h >
int bind(int sock, const struct sockaddr* addr, socklen_t addrlen);
有了socket句柄,我們需要將句柄綁定到某個IP上,所以參數(shù)分別是通過socket
創(chuàng)建的句柄和轉(zhuǎn)換后的struct sockaddr
。
注意:
(1)返回錯誤errno=EACCES
:被綁定的地址是受保護的,比如端口0-1023不允許使用;
(2)返回錯誤errno=EADDRINUSE
:被綁定的地址正在使用,比如socket被其他已經(jīng)綁定了或者TIME_WAIT
階段;
5、listen
#include < sys/socket.h >
int listen(int sock, int backlog);
(1)sock
是socket
的句柄;
(2)backlog
在上一篇文章中講過,是處于半連接和完全連接的sock
上限;
6、accept
#include < sys/types.h >
#include < sys/socket.h >
int accept(int sock, struct sockaddr *addr, socklen_t addrlen);
(1)sock
是socket
的句柄;
(2)addr
用來獲取建立連接后的對端的地址;
詳細的accept
建立連接流程,在上一篇文章也有詳細講過(可以重新翻閱一下), 這里要注意的是accept
應(yīng)該如何和與高性能結(jié)合,這里留個疑問,下一篇文章將會介紹《IO復(fù)用》會詳細介紹。
7、connect
#include < sys/types.h >
#include < sys/socket.h >
int connect(int sock, const struct sockaddr *addr, socklen_t addrlen);
client端發(fā)起連接的函數(shù),sock
是socket
的句柄,addr
連接的唯一地址,這個函數(shù)使用的注意事項:
(1)返回ECONNREFUSED
,標識目標端口不存在,連接被拒絕;
(2)返回ETIMEOUT
,連接超時;
8、close和shutdown
#include < unistd.h >
int close(int fd);
int shutdown(int sockfd, int flag);
這兩個函數(shù)的區(qū)別也在上一篇文章有提及,close
不是真正關(guān)閉連接,只有fd引用計數(shù)為0才關(guān)閉,shutdown
立即終止連接。
注意:
(1)shutdown
的flag=SHUT_RD
,關(guān)閉連接的讀端,不再執(zhí)行讀操作,socket的緩沖區(qū)數(shù)據(jù)都被清空;
(2)shutdown
的flag=SHUT_WR
,關(guān)閉連接的寫端,不再執(zhí)行寫操作,socket的緩沖區(qū)數(shù)據(jù)會在關(guān)閉之前全部發(fā)送出去;
(3)shutdown
的flag=SHUT_RDWR
,關(guān)閉連接的讀端和寫端,其緩沖區(qū)數(shù)據(jù)處理如上;
9、讀寫數(shù)據(jù)
TCP讀寫數(shù)據(jù):
#include < sys/types.h >
#include < sys/socket.h >
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);
這里要注意的是一些flags的使用:
(1)flags=MSG_OOB
發(fā)送或者接收緊急數(shù)據(jù);
(2)flags=MSG_DONTWAIT
對socket此次操作不阻塞;
(3)flags=MSG_WAITALL
讀到指定大小的字節(jié)才返回;
(4)flags=MSG_MORE
告訴內(nèi)核還有更多數(shù)據(jù)發(fā)送,讓內(nèi)核等數(shù)據(jù)一起發(fā)送提升性能;
UDP讀寫數(shù)據(jù):
#include < sys/types.h >
#include < sys/socket.h >
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *addr, socklen_t *addrlen);
由于UDP是無連接的,所以不需要connect
或者accept
直接填addr地址發(fā)送或者接收數(shù)據(jù)。
10、獲取地址信息
#include < sys/socket.h >
int getsockname(int sock, const struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sock, const struct sockaddr *addr, socklen_t *addrlen);
(1)getsockname
通過fd獲取【本端】的socket
地址;
(2)getpeername
通過fd獲取【對端】的socket
地址;
11、一些socket選項
(1)SO_REUSEADDR
強制處于TIME_WAIT
狀態(tài)的socket句柄可以被bind;
(2)SO_RECVBUF
和SO_SENDBUF
設(shè)置socket句柄的發(fā)送緩沖區(qū)和接收緩沖區(qū)的大小;
(3)SO_RECVLOWAT
和SO_SNDLOWAT
設(shè)置句柄在緩沖區(qū)觸發(fā)I/O事件的大小,接收低潮限度和發(fā)送低潮限度默認為1字節(jié);(4)SO_LINGER
用于控制close
系統(tǒng)調(diào)用在關(guān)閉TCP連接時的行為,其結(jié)構(gòu)體:
#include < sys/socket.h >
struct linger
{
int l_onoff; // 開啟(非0)還是關(guān)閉(0)該選項
int l_linger; // 滯留時間
};
// 1、l_onoff等于0(關(guān)閉),此時SO_LINGER選項不起作用,close用默認行為來關(guān)閉socket;
// 2、l_onoff不為0(開啟),l_linger等于0,此時close系統(tǒng)調(diào)用立即返回,TCP模塊將丟棄被關(guān)閉的socket對應(yīng)的TCP發(fā)送緩沖區(qū)中殘留的數(shù)據(jù),同時給對方發(fā)送一個復(fù)位報文段(RST);
// 3、l_onoff不為0(開啟),l_linger大于0,此時close的行為取決于兩個條件:一是被關(guān)閉的socket對應(yīng)的TCP發(fā)送緩沖區(qū)是否還有殘留的數(shù)據(jù);二是該socket是阻塞的,還是非阻塞的,對于阻塞的socket,close將等待一段長為l_linger的時間,直到TCP模塊發(fā)送完所有殘留數(shù)據(jù)并得到對方的確認;如果這段時間內(nèi)TCP模塊沒有發(fā)送完殘留數(shù)據(jù)并得到對方的確認,那么close系統(tǒng)調(diào)用將返回-1并設(shè)置errno為EWOULDBLOCK;如果socket是非阻塞的,close將立即返回,此時我們需要根據(jù)其返回值和errno來判斷殘留數(shù)據(jù)是否已經(jīng)發(fā)送完畢;
第二部分:I/O函數(shù)
1、pipe
pipe
作為IPC的一部分,其參數(shù)如下:
#include < unistd.h >
int pipe(int fd[2]);
通過fd[0]和fd[1]組成了管道的兩端,fd[0]只能讀出數(shù)據(jù),fd[1]只能寫入數(shù)據(jù),配合read
和write
使用,當然管道的容量是有限制的(默認是65536字節(jié)),可以通過fnctl修改大小。
2、socketpair
對比管道,我覺得socketpair
更加方便,其參數(shù)如下:
#include< sys/types.h >
#include< sys/socket.h >
int socketpair(int domain, int type, int protocol, int fd[2]);
其中fd[2]和pipe
一樣,不同的是可以讀也可以寫,domain參數(shù)設(shè)置為AF_UNIX
。
3、dup和dup2
#include< unistd.h >
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup
函數(shù)創(chuàng)建一個新的文件描述符,該新文件描述符和原有文件描述符oldfd
指向相同的文件、管道或者網(wǎng)絡(luò)連接。并且dup
返回的文件描述符總是取系統(tǒng)當前可用的最小整數(shù)值;dup2
和dup
類似,不過它將返回第一個不小于newfd
的整數(shù)值的文件描述符,并且newfd這個文件描述符也將會指向oldfd
指向的文件,原來的newfd
指向的文件將會被關(guān)閉(除非newfd
和oldfd
相同),相比于dup
函數(shù),dup2
函數(shù)它的優(yōu)勢就是可以指定新的文件描述符的大小,用法比較靈活;
樣例如下:
#include < stdio.h >
#include < sys/types.h >
#include < sys/stat.h >
#include < fcntl.h >
#include < unistd.h >
#define FILENAME "test.txt"
int main(void)
{
int fd1 = -1, fd2 = -1;
fd1 = open(FILENAME, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 < 0)
{
return -1;
}
printf("fd1 = %d.n", fd1);
fd2 = dup2(fd1, 10);
printf("fd2 = %d.n", fd2);
close(fd1);
return 0;
}
// 輸出
fd2 = 10
4、readv和writev
#include < sys/uio.h >
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
fd
被操作的目標文件描述符,iov
是iovec類型的數(shù)組,iovcnt
是iov數(shù)組的長度,iovec
結(jié)構(gòu)體封裝了一塊內(nèi)存的起始位置和長度。readv
和writev
的目的將分散的內(nèi)存數(shù)據(jù)集中讀寫到文件描述符中,可以提升性能。
writev樣例如下:
...
char *str0 = "this is 0 ";
char *str1 = "this is 1";
struct iovec iov[2];
ssize_t nwritten;
iov[0].iov_base = str0;
iov[0].iov_len = strlen(str0);
iov[1].iov_base = str1;
iov[1].iov_len = strlen(str1);
nwritten = writev(STDOUT_FILENO, iov, sizeof(iov));
...
readv樣例如下:
...
char buf1[8] = { 0 };
char buf2[8] = { 0 };
struct iovec iov[2];
ssize_t nread;
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1) - 1;
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2) - 1;
nread = readv(STDIN_FILENO, iov, 2);
...
5、sendfile
通常對于文件的讀寫然后發(fā)送出去,會經(jīng)過磁盤->內(nèi)核態(tài)拷貝->用戶態(tài)read->用戶態(tài)write->內(nèi)核態(tài)拷貝->DMA,那么這里經(jīng)過多次上下文切換和拷貝,所以sendfile系統(tǒng)函數(shù)為了避免這些問題,實現(xiàn)零拷貝。
#include < sys/sendfile.h >
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
(1)out_fd
待讀出的文件fd,必須是一個socket句柄;
(2)in_fd
待寫入的文件fd,必須是文件描述符,不能是管道或者socket句柄;
6、splice
splice用于在兩個文件描述符之間移動數(shù)據(jù),也是一種重要零拷貝技術(shù)。
#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);
(1)fd_in
待輸入數(shù)據(jù)的文件描述符,如果fd_in
是一個管道文件,那么off_in
必須被設(shè)置為NULL;如果不是,那么off_in
表示從輸入數(shù)據(jù)流的何處開始讀取數(shù)據(jù),此時,若off_in
被設(shè)置為NULL,則表示從輸入數(shù)據(jù)流的當前偏移位置讀入;若off_in
不為NULL,則將指出具體的偏移位置;
(2)fd_out/off_out
參數(shù)含義與fd_in/off_in
相同,不過用于輸出流;
評論
查看更多