以下內(nèi)容部分經(jīng)過驗(yàn)證草讶,可能有些地方需要讀者按照例子自己驗(yàn)證
一祭椰、linux 網(wǎng)絡(luò)io相關(guān)函數(shù)
linux 關(guān)于io(包括網(wǎng)絡(luò)io)的操作泥张,都抽象成是面向文件的模型莹痢。
1种蘸、文件描述符
linux下文件描述符是int的整數(shù)值,前兩個被系統(tǒng)占用(終端竞膳,和異常)航瞭。
關(guān)于網(wǎng)絡(luò)套接字的io,服務(wù)端和客戶端坦辟,第一步都是調(diào)用socket函數(shù)創(chuàng)建 ‘套接字’刊侯,返回一個文件‘描述符fd’。也可以理解為創(chuàng)建了一個文件锉走。
服務(wù)端的這個‘文件描述符’滨彻,用于接收連接請求,通過accept函數(shù)挪蹭。
客戶端的這個‘文件描述符’ 亭饵,用于客戶端向服務(wù)端發(fā)送和接收數(shù)據(jù),通過read/recv和write/send函數(shù) 梁厉。
(1)辜羊、創(chuàng)建網(wǎng)絡(luò)傳輸根文件描述符:socket函數(shù)
int socket(int domain, int type, int protocol); 返回的描述符,默認(rèn)是阻塞模式词顾,若type參數(shù)傳入 (xxx |SOCK_NONBLOCK) 就是非阻塞的八秃,也可以用文件描述符通過fcntl() 函數(shù)或 ioctl() 函數(shù),將套接字設(shè)置成非阻塞的肉盹。
舉例1:
socket(AF_INET,SOCK_STREAM|SOCK_NONBLOCK,0)
舉例2:
int flags = fcntl('文件描述符', F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl('文件描述符', F_SETFL, flags);
(2)昔驱、服務(wù)端開啟監(jiān)聽函數(shù): listen('文件描述符', 'tcp 連接隊(duì)列長度');
關(guān)于 listen 函數(shù)的最后一個參數(shù),不同的linux版本上忍,代表不同的含義舍悯。在Linux內(nèi)核2.2之后航棱,listen的最后一個參數(shù)(socket backlog)的形為是指等待accept的完全建立(tcp狀態(tài)ESTABLISHED )的套接字的隊(duì)列長度,不包括不完全(tcp 狀態(tài) SYN RECEIVED )連接請求的數(shù)量萌衬。 不完全連接的長度可以使用系統(tǒng)參數(shù) /proc/sys/net/ipv4/tcp_max_syn_backlog設(shè)置饮醇。
(3)、服務(wù)端接受連接: accept函數(shù)
accept('socket 函數(shù)返回的描述符', '出參: 客戶端ip地址', '地址長度')秕豫,返回值:服務(wù)器端為客戶端創(chuàng)建的文件描述符朴艰。服務(wù)端向客戶端發(fā)送和接受數(shù)據(jù)是使用。是否為阻塞的和'socket 函數(shù)返回的描述符'一致混移,也可以用accept4函數(shù)返回的文件描述符為非阻塞的祠墅。或通過fcntl() 函數(shù)設(shè)置為非阻塞的歌径。
舉例1:
int accept4(sockfd,(struct sockaddr*)&cli,&len,SOCK_NONBLOCK);
(4)毁嗦、客戶端主動連接服務(wù)端: connect 函數(shù)
次函數(shù)會經(jīng)過tcp三次握手,若sockfd文件操作符回铛,非阻塞方式不等三次握手完成的終止態(tài)ESTABLISHED,connect 的函數(shù)就會返回小于0的值狗准。
if((connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))<0){
printf("tcp 三次握手未完成,或錯誤無!");
//exit(1);
}
2茵肃、socket讀寫操作
(1)腔长、讀操作
linux 為讀操作提供了:read(文件描述符, 緩沖, 緩沖長度)、recv(文件描述符, 緩沖, 緩沖長度,讀取類型) 函數(shù)验残。
read函數(shù):如果入?yún)ⅰ募枋龇?是阻塞的捞附,即為阻塞模式讀取。如果入?yún)ⅰ募枋龇?是非阻塞的您没,就以非阻塞方式讀取鸟召。
recv函數(shù):同read函數(shù)類似,區(qū)別是‘讀取類型’ 參數(shù)氨鹏。決定是否為阻塞讀取
舉例1:
recv(sockfd, buff, buff_size,MSG_DONTWAIT);
入?yún)ⅲ?br> MSG_DONTWAIT:這個標(biāo)志將單個IO操作設(shè)為非堵塞方式药版,而不需要在套接字上打開非堵塞的標(biāo)志,執(zhí)行IO操作喻犁。然后關(guān)閉非堵塞的標(biāo)志槽片。
MSG_WAITALL:這個標(biāo)志告訴內(nèi)核在沒有讀到請求的字節(jié)數(shù)之前不使讀操作返回。
注意:
使用MSG_WAITALL時肢础,’文件描述符‘ 必須處于阻塞模式下还栓,否則不起作用。所MSG_WAITALL不能和MSG_NONBLOCK同時使用传轰。
返回值: 成功執(zhí)行時剩盒,返回接收到的字節(jié)數(shù)。另一端已關(guān)閉則返回0慨蛙。失敗返回-1辽聊,errno被設(shè)為以下的某個值
EAGAIN:套接字已標(biāo)記為非阻塞纪挎,而接收操作被阻塞或者接收超時EBADF:sock不是有效的描述詞
ECONNREFUSE:遠(yuǎn)程主機(jī)阻絕網(wǎng)絡(luò)連接
EFAULT:內(nèi)存空間訪問出錯
EINTR:操作被信號中斷
EINVAL:參數(shù)無效
ENOMEM:內(nèi)存不足
ENOTCONN:與面向連接關(guān)聯(lián)的套接字尚未被連接上
ENOTSOCK:sock索引的不是套接字
(2)、linux 為寫操作提供了:write跟匆、send函數(shù)
(3) 讀寫函數(shù)總結(jié):
1> send 函數(shù):并不是往網(wǎng)絡(luò)上發(fā)送數(shù)據(jù)异袄,而是將應(yīng)用層發(fā)送緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)(網(wǎng)卡緩沖區(qū))中去,至于什么時候數(shù)據(jù)會從網(wǎng)卡緩沖區(qū)中真正地發(fā)到網(wǎng)絡(luò)中去要根據(jù) TCP/IP 協(xié)議棧的行為來確定玛臂,這種行為涉及到一個叫 nagel 算法和 TCP_NODELAY 的 socket 選項(xiàng)烤蜕。
2> recv 函數(shù):不是從網(wǎng)絡(luò)上收取數(shù)據(jù),而只是將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到應(yīng)用程序的緩沖區(qū)中迹冤,拷貝完成以后會將內(nèi)核緩沖區(qū)中該部分?jǐn)?shù)據(jù)移除讽营。
二、linux socket阻塞和非阻塞有哪些不同(引用)
- 建立連接
阻塞方式下泡徙,客戶端 connect首先發(fā)送SYN請求到服務(wù)器橱鹏,當(dāng)客戶端收到服務(wù)器返回的SYN的確認(rèn)時,則connect返回堪藐,否則的話一直阻塞莉兰。
非阻塞方式,connect將啟用TCP協(xié)議的三次握手庶橱,但是connect函數(shù)并不等待連接建立好才返回贮勃,而是立即返回贪惹,返回的錯誤碼為EINPROGRESS,表示正在進(jìn)行某種過程苏章。 - 接收連接
阻塞模式下調(diào)用accept()函數(shù),而且沒有新連接時奏瞬,進(jìn)程會進(jìn)入睡眠狀態(tài)枫绅,直到有可用的連接,才返回硼端。
非阻塞模式下調(diào)用accept()函數(shù)立即返回并淋,有連接返回客戶端套接字描述符,沒有新連接時珍昨,將返回EWOULDBLOCK錯誤碼县耽,表示本來應(yīng)該阻塞。 - 讀操作
阻塞模式下調(diào)用read(),recv()等讀套接字函數(shù)會一直阻塞住镣典,直到有數(shù)據(jù)到來才返回兔毙。當(dāng)socket緩沖區(qū)中的數(shù)據(jù)量小于期望讀取的數(shù)據(jù)量時,返回實(shí)際讀取的字節(jié)數(shù)兄春。當(dāng)sockt的接收緩沖區(qū)中的數(shù)據(jù)大于期望讀取的字節(jié)數(shù)時澎剥,讀取期望讀取的字節(jié)數(shù),返回實(shí)際讀取的長度赶舆。
對于非阻塞socket而言哑姚,socket的接收緩沖區(qū)中有沒有數(shù)據(jù)祭饭,read調(diào)用都會立刻返回。接收緩沖區(qū)中有數(shù)據(jù)時叙量,與阻塞socket有數(shù)據(jù)的情況是一樣的倡蝙,如果接收緩沖區(qū)中沒有數(shù)據(jù),則返回錯誤號為EWOULDBLOCK宛乃,表示該操作本來應(yīng)該阻塞的悠咱,但是由于本socket為非阻塞的socket,因此立刻返回征炼。遇到這樣的情況析既,可以在下次接著去嘗試讀取。如果返回值是其它負(fù)值谆奥,則表明讀取錯誤眼坏。 - 寫操作
對于阻塞Socket而言,如果發(fā)送緩沖區(qū)沒有空間或者空間不足的話酸些,write操作會直接阻塞住宰译,如果有足夠空間,則拷貝所有數(shù)據(jù)到發(fā)送緩沖區(qū)魄懂,然后返回.
對于寫操作write,原理和read是類似的沿侈,非阻塞socket在發(fā)送緩沖區(qū)沒有空間時會直接返回錯誤號EWOULDBLOCK,表示沒有空間可寫數(shù)據(jù),如果錯誤號是別的值市栗,則表明發(fā)送失敗缀拭。如果發(fā)送緩沖區(qū)中有足夠空間或者是不足以拷貝所有待發(fā)送數(shù)據(jù)的空間的話,則拷貝前面N個能夠容納的數(shù)據(jù)填帽,返回實(shí)際拷貝的字節(jié)數(shù)蛛淋。
尤其注意非阻塞的socket,在建立連接時要兼容處理返回EINPROGRESS情況篡腌,在接收連接褐荷、讀操作、寫操作時要兼容處理返回EWOULDBLOCK錯誤碼的情況嘹悼。
三叛甫、linux select 方法實(shí)現(xiàn)的服務(wù)端
//服務(wù)端程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>//hotn
#include <unistd.h>
#include <sys/select.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
//文件描述符(file descriptor)是內(nèi)核為文件所創(chuàng)建的索引
//Linux剛啟動的時候會自動設(shè)置0是標(biāo)準(zhǔn)輸入,1是標(biāo)準(zhǔn)輸出杨伙,2是標(biāo)準(zhǔn)錯誤其监。
int main(int argc, char const *argv[])
{
//判斷入?yún)? if (argc<2){
printf("eg: ./a.out prot\n");
exit(1);
}
//atoi 類型轉(zhuǎn)換
int port = atoi(argv[1]);
//開啟服務(wù)端 socket文件描述符,用戶標(biāo)記新連接的接收
int lfd = socket(AF_INET, SOCK_STREAM, 0);
//初始化服務(wù)器 sockaddr_in
struct sockaddr_in serverAddr;
socklen_t serverAddrLen= sizeof(serverAddr);
memset(&serverAddr,0,serverAddrLen);
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(port);
//將文件描述符綁定端口和地址
bind(lfd, (struct sockaddr *)&serverAddr, serverAddrLen);
//開啟監(jiān)聽,設(shè)置最大
listen(lfd, 3);
printf("select io:Start accept .....\n");
//客戶端地址 聲明
struct sockaddr_in clientAddr;
socklen_t clientAddrLen= sizeof(clientAddr);
//-------------- select 相關(guān)代碼----------------------------------------
int MAX_SOCK_FD_INDEX = 12;
//超時時間
struct timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 1000;
//可讀取的文件描述符集合
fd_set readFds;
//初始文件描述符'標(biāo)記'集合 is_connected 數(shù)組的index為文件描述符‘索引’
int isConnected[MAX_SOCK_FD_INDEX];
for(int i = 0; i < MAX_SOCK_FD_INDEX; i++)
isConnected[i] = 0;
while(1){
//將讀操作集合重置
FD_ZERO(&readFds);
//將服務(wù)端描述符缀台,設(shè)置為可讀操作
FD_SET(lfd, &readFds);
//將準(zhǔn)備就緒的棠赛,文件描述,設(shè)置為可讀操作
for(int i= 0; i < MAX_SOCK_FD_INDEX; i++)
if(isConnected[i])
FD_SET(i, &readFds);
if(!select(MAX_SOCK_FD_INDEX, &readFds, NULL, NULL, &timeout))
//如果超時那么跳過循環(huán)
continue;
//循環(huán)所有監(jiān)聽的描述符
for(int i = 0; i < MAX_SOCK_FD_INDEX; i++){
//如果文件描述符是可讀的
if(FD_ISSET(i, &readFds)){
int fd = i;
//如果可讀的文件描述符為 '根文件描述符' ,那么說明是-----新的連接
if(lfd == fd){
int cfd = accept(lfd, (struct sockaddr *)&clientAddr, &clientAddrLen);
if(cfd ==-1){
perror("accpet error");
exit(0);
}
//向新文件描述符(連接)寫入數(shù)據(jù)
//write(cfd, "hello world", sizeof("hello world"));
//將新文件描述符設(shè)置為 可讀
isConnected[cfd] = 1;
//打印客戶端地址
char ip[64] = {0};
printf("new Clinet ip %s, Port %d \n",
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,sizeof(ip)),
ntohs(clientAddr.sin_port)
);
}
//否則為 久文件描述符睛约,那么是可讀事件
else{
char buf[256];
bzero(buf, sizeof(buf));
//從文件描述符鼎俘,讀取信息, 如果沒有讀到數(shù)據(jù)說明辩涝,連接斷了
if(read(fd, buf , sizeof(buf)) <= 0){
printf("Connection closed.\n");
//取消文件描述符的可讀狀態(tài)
isConnected[fd] = 0;
//關(guān)閉文件描述符
close(fd);
}
else{
//打印客戶端輸入的內(nèi)容
printf("%s", buf);
}
}
}
}
}
}
四贸伐、linux epoll 方法實(shí)現(xiàn)的服務(wù)端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>//hotn
#include <unistd.h>
#include <sys/epoll.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
if (argc<2) {
printf("eg: ./a.out prot\n");
exit(1);
}
//atoi 類型轉(zhuǎn)換
int port = atoi(argv[1]);
//創(chuàng)建套接字,文件描述符怔揩,接受連接的節(jié)點(diǎn)
int lfd = socket(AF_INET,SOCK_STREAM,0);
//服務(wù)端地址
struct sockaddr_in serverAddr;
socklen_t serverAddrlen= sizeof(serverAddr);
//初始化服務(wù)器 sockaddr_in
memset(&serverAddr,0,serv_len);
//地址族
serverAddr.sin_family=AF_INET;
//設(shè)置監(jiān)聽本機(jī)ip
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
//設(shè)置監(jiān)聽端口
serverAddr.sin_port=htons(port);
//綁定ip
bind(lfd,(struct sockaddr *)&serverAddr,serverAddrlen);
//監(jiān)聽 最大值128
//在Linux內(nèi)核2.2之后捉邢,socket backlog參數(shù)(listen的最后一個參數(shù))的形為改變了,
//現(xiàn)在它指等待accept的完全建立(ESTABLISHED 狀態(tài))的套接字的隊(duì)列長度商膊,而不是不完全(SYN RECEIVED 狀態(tài))連接請求的數(shù)量伏伐。
//不完全連接的長度可以使用/proc/sys/net/ipv4/tcp_max_syn_backlog設(shè)置。
listen(lfd,128);
printf("epoll io:Start accept .....\n");
//客戶端地址 聲明
struct sockaddr_in clientAddr;
socklen_t clientAddrLen= sizeof(clientAddr);
//-------------- epoll 相關(guān)代碼----------------------------------------
//創(chuàng)建epoll樹, 初始創(chuàng)建2000個子節(jié)點(diǎn)晕拆, 可擴(kuò)容
int epfd = epoll_create(2000);
//初始化藐翎,根節(jié)點(diǎn) 關(guān)注的事件
struct epoll_event ev;
//讀寫事件
ev.events=EPOLLIN;
ev.data.fd=lfd;
//初始化epoll數(shù)根節(jié)點(diǎn)
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
//聲明 內(nèi)核返回的,檢測到的事件數(shù)組
struct epoll_event all[2000];
while(1){
//使用epoll 通知內(nèi)核 文件io檢測,第3參數(shù)為數(shù)組大小,最后一個參數(shù)-1代表阻塞
//返回值為实幕,有多少個元素發(fā)生了吝镣,io事件
int ret = epoll_wait(epfd,all,sizeof(all)/sizeof(0),-1);
for (int i = 0; i < ret; ++i) {
int fd=all[i].data.fd;
if(fd == lfd){//新連接
//接受連接請求
int cfd=accept(lfd,(struct sockaddr *)&clientAddr, &clientAddrLen);
if(cfd ==-1){
perror("accpet error");
exit(0);
}
//初始化,普通節(jié)點(diǎn) 關(guān)注的事件
struct epoll_event tmp;
//讀寫事件
tmp.events=EPOLLIN;
tmp.data.fd=cfd;
//將新得到的cfd掛在樹上
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&tmp);
//打印客戶端信息
char ip[64] = {0};
printf("new Clinet ip %s, Port %d \n",
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,sizeof(ip)),
ntohs(clientAddr.sin_port)
);
}else {
//處理已連接的客戶端發(fā)來的數(shù)據(jù)
char buf[1024] ={0};
int len = recv(fd, buf, sizeof(buf), 0);
if(len == -1){
perror("recv error");
}else if(len == 0){
printf("clent close");
close(fd);
//將fd從樹上刪除
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}else{
printf("recv buf %s\n", buf);
//將 buf 回寫客戶端
write(fd,buf,len);
}
}
}
}
close(lfd);
return 0;
}
五昆庇、linux 客戶端
//客戶端程序
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <ctype.h>
// socket編程中write末贾、read和send、recv之間的區(qū)別 , 在于第四個參數(shù)recv可以控制是否整吆,讀取完成后是否刪除緩沖
void main(int argc, char const *argv[]){
//判斷入?yún)? if (argc<2 || argc>3){
printf("eg: ./a.out [ip] prot\n");
exit(1);
}
const char *ip="127.0.0.1";
int port;
if (argc==3){
ip = argv[1];
port = atoi(argv[2]);
}else{
port = atoi(argv[1]);
}
//連接地址
struct sockaddr_in addr;
//創(chuàng)建根文件描述符
int sockfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&addr,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
//創(chuàng)建連接拱撵,此處三次握手,如果socket 創(chuàng)建時掂为,參數(shù)為非阻塞裕膀,那么此處不等三次握手完成员串,立即返回勇哗。
if((connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))<0){
printf("tcp 三次握手未完成,或錯誤無!");
exit(1);
}
//讀取數(shù)據(jù)的大小
char buf[256];
//從根文件描述符讀取數(shù)據(jù)寸齐,最后一個參數(shù)可設(shè)置欲诺,是否阻塞模式
//注意協(xié)議接收到的數(shù)據(jù)可能大于buf的長度,
//所以在這種情況下要調(diào)用幾次recv函數(shù)才能把s的接收緩沖中的數(shù)據(jù)copy完渺鹦。recv函數(shù)僅僅是copy數(shù)據(jù)扰法,真正的接收數(shù)據(jù)是協(xié)議來完成的
recv(sockfd, buf, sizeof(buf), 10);
printf("%s\n",buf);
printf("please in put information:\n");
while(1){
bzero(buf,sizeof(buf));
/* printf("%s\n","非阻塞 讀取");
int flags;
//使用非阻塞io
if(flags = fcntl(STDIN_FILENO, F_GETFL, 0) < 0)
{
perror("fcntl");
return -1;
}
flags |= O_NONBLOCK;
if(fcntl(STDIN_FILENO, F_SETFL, flags) < 0)
{
perror("fcntl");
return -1;
}*/
//鍵盤讀入.默認(rèn)為阻塞
read(STDIN_FILENO, buf, sizeof(buf));
//寫入到 根文件描述符
if(send(sockfd, buf, sizeof(buf), 0) < 0){
perror("send error!");
exit(1);
}
}
}