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