以下內(nèi)容部分經(jīng)過驗證,可能有些地方需要讀者按照例子自己驗證
一、linux 網(wǎng)絡io相關函數(shù)
linux 關于io(包括網(wǎng)絡io)的操作,都抽象成是面向文件的模型。
1、文件描述符
linux下文件描述符是int的整數(shù)值,前兩個被系統(tǒng)占用(終端,和異常)。
關于網(wǎng)絡套接字的io,服務端和客戶端,第一步都是調(diào)用socket函數(shù)創(chuàng)建 ‘套接字’,返回一個文件‘描述符fd’。也可以理解為創(chuàng)建了一個文件。
服務端的這個‘文件描述符’,用于接收連接請求,通過accept函數(shù)。
客戶端的這個‘文件描述符’ ,用于客戶端向服務端發(fā)送和接收數(shù)據(jù),通過read/recv和write/send函數(shù) 。
(1)、創(chuàng)建網(wǎng)絡傳輸根文件描述符:socket函數(shù)
int socket(int domain, int type, int protocol); 返回的描述符,默認是阻塞模式,若type參數(shù)傳入 (xxx |SOCK_NONBLOCK) 就是非阻塞的,也可以用文件描述符通過fcntl() 函數(shù)或 ioctl() 函數(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)、服務端開啟監(jiān)聽函數(shù): listen('文件描述符', 'tcp 連接隊列長度');
關于 listen 函數(shù)的最后一個參數(shù),不同的linux版本,代表不同的含義。在Linux內(nèi)核2.2之后,listen的最后一個參數(shù)(socket backlog)的形為是指等待accept的完全建立(tcp狀態(tài)ESTABLISHED )的套接字的隊列長度,不包括不完全(tcp 狀態(tài) SYN RECEIVED )連接請求的數(shù)量。 不完全連接的長度可以使用系統(tǒng)參數(shù) /proc/sys/net/ipv4/tcp_max_syn_backlog設置。
(3)、服務端接受連接: accept函數(shù)
accept('socket 函數(shù)返回的描述符', '出參: 客戶端ip地址', '地址長度'),返回值:服務器端為客戶端創(chuàng)建的文件描述符。服務端向客戶端發(fā)送和接受數(shù)據(jù)是使用。是否為阻塞的和'socket 函數(shù)返回的描述符'一致,也可以用accept4函數(shù)返回的文件描述符為非阻塞的。或通過fcntl() 函數(shù)設置為非阻塞的。
舉例1:
int accept4(sockfd,(struct sockaddr*)&cli,&len,SOCK_NONBLOCK);
(4)、客戶端主動連接服務端: 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:這個標志將單個IO操作設為非堵塞方式,而不需要在套接字上打開非堵塞的標志,執(zhí)行IO操作。然后關閉非堵塞的標志。
MSG_WAITALL:這個標志告訴內(nèi)核在沒有讀到請求的字節(jié)數(shù)之前不使讀操作返回。
注意:
使用MSG_WAITALL時,’文件描述符‘ 必須處于阻塞模式下,否則不起作用。所MSG_WAITALL不能和MSG_NONBLOCK同時使用。
返回值: 成功執(zhí)行時,返回接收到的字節(jié)數(shù)。另一端已關閉則返回0。失敗返回-1,errno被設為以下的某個值
EAGAIN:套接字已標記為非阻塞,而接收操作被阻塞或者接收超時EBADF:sock不是有效的描述詞
ECONNREFUSE:遠程主機阻絕網(wǎng)絡連接
EFAULT:內(nèi)存空間訪問出錯
EINTR:操作被信號中斷
EINVAL:參數(shù)無效
ENOMEM:內(nèi)存不足
ENOTCONN:與面向連接關聯(lián)的套接字尚未被連接上
ENOTSOCK:sock索引的不是套接字
(2)、linux 為寫操作提供了:write、send函數(shù)
(3) 讀寫函數(shù)總結:
1> send 函數(shù):并不是往網(wǎng)絡上發(fā)送數(shù)據(jù),而是將應用層發(fā)送緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)(網(wǎng)卡緩沖區(qū))中去,至于什么時候數(shù)據(jù)會從網(wǎng)卡緩沖區(qū)中真正地發(fā)到網(wǎng)絡中去要根據(jù) TCP/IP 協(xié)議棧的行為來確定,這種行為涉及到一個叫 nagel 算法和 TCP_NODELAY 的 socket 選項。
2> recv 函數(shù):不是從網(wǎng)絡上收取數(shù)據(jù),而只是將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到應用程序的緩沖區(qū)中,拷貝完成以后會將內(nèi)核緩沖區(qū)中該部分數(shù)據(jù)移除。
二、linux socket阻塞和非阻塞有哪些不同(引用)
- 建立連接
阻塞方式下,客戶端 connect首先發(fā)送SYN請求到服務器,當客戶端收到服務器返回的SYN的確認時,則connect返回,否則的話一直阻塞。
非阻塞方式,connect將啟用TCP協(xié)議的三次握手,但是connect函數(shù)并不等待連接建立好才返回,而是立即返回,返回的錯誤碼為EINPROGRESS,表示正在進行某種過程。 - 接收連接
阻塞模式下調(diào)用accept()函數(shù),而且沒有新連接時,進程會進入睡眠狀態(tài),直到有可用的連接,才返回。
非阻塞模式下調(diào)用accept()函數(shù)立即返回,有連接返回客戶端套接字描述符,沒有新連接時,將返回EWOULDBLOCK錯誤碼,表示本來應該阻塞。 - 讀操作
阻塞模式下調(diào)用read(),recv()等讀套接字函數(shù)會一直阻塞住,直到有數(shù)據(jù)到來才返回。當socket緩沖區(qū)中的數(shù)據(jù)量小于期望讀取的數(shù)據(jù)量時,返回實際讀取的字節(jié)數(shù)。當sockt的接收緩沖區(qū)中的數(shù)據(jù)大于期望讀取的字節(jié)數(shù)時,讀取期望讀取的字節(jié)數(shù),返回實際讀取的長度。
對于非阻塞socket而言,socket的接收緩沖區(qū)中有沒有數(shù)據(jù),read調(diào)用都會立刻返回。接收緩沖區(qū)中有數(shù)據(jù)時,與阻塞socket有數(shù)據(jù)的情況是一樣的,如果接收緩沖區(qū)中沒有數(shù)據(jù),則返回錯誤號為EWOULDBLOCK,表示該操作本來應該阻塞的,但是由于本socket為非阻塞的socket,因此立刻返回。遇到這樣的情況,可以在下次接著去嘗試讀取。如果返回值是其它負值,則表明讀取錯誤。 - 寫操作
對于阻塞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ù),返回實際拷貝的字節(jié)數(shù)。
尤其注意非阻塞的socket,在建立連接時要兼容處理返回EINPROGRESS情況,在接收連接、讀操作、寫操作時要兼容處理返回EWOULDBLOCK錯誤碼的情況。
三、linux select 方法實現(xiàn)的服務端
//服務端程序
#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剛啟動的時候會自動設置0是標準輸入,1是標準輸出,2是標準錯誤。
int main(int argc, char const *argv[])
{
//判斷入?yún)? if (argc<2){
printf("eg: ./a.out prot\n");
exit(1);
}
//atoi 類型轉換
int port = atoi(argv[1]);
//開啟服務端 socket文件描述符,用戶標記新連接的接收
int lfd = socket(AF_INET, SOCK_STREAM, 0);
//初始化服務器 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)聽,設置最大
listen(lfd, 3);
printf("select io:Start accept .....\n");
//客戶端地址 聲明
struct sockaddr_in clientAddr;
socklen_t clientAddrLen= sizeof(clientAddr);
//-------------- select 相關代碼----------------------------------------
int MAX_SOCK_FD_INDEX = 12;
//超時時間
struct timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 1000;
//可讀取的文件描述符集合
fd_set readFds;
//初始文件描述符'標記'集合 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);
//將服務端描述符,設置為可讀操作
FD_SET(lfd, &readFds);
//將準備就緒的,文件描述,設置為可讀操作
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"));
//將新文件描述符設置為 可讀
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;
//關閉文件描述符
close(fd);
}
else{
//打印客戶端輸入的內(nèi)容
printf("%s", buf);
}
}
}
}
}
}
四、linux epoll 方法實現(xiàn)的服務端
#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 類型轉換
int port = atoi(argv[1]);
//創(chuàng)建套接字,文件描述符,接受連接的節(jié)點
int lfd = socket(AF_INET,SOCK_STREAM,0);
//服務端地址
struct sockaddr_in serverAddr;
socklen_t serverAddrlen= sizeof(serverAddr);
//初始化服務器 sockaddr_in
memset(&serverAddr,0,serv_len);
//地址族
serverAddr.sin_family=AF_INET;
//設置監(jiān)聽本機ip
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
//設置監(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))的套接字的隊列長度,而不是不完全(SYN RECEIVED 狀態(tài))連接請求的數(shù)量。
//不完全連接的長度可以使用/proc/sys/net/ipv4/tcp_max_syn_backlog設置。
listen(lfd,128);
printf("epoll io:Start accept .....\n");
//客戶端地址 聲明
struct sockaddr_in clientAddr;
socklen_t clientAddrLen= sizeof(clientAddr);
//-------------- epoll 相關代碼----------------------------------------
//創(chuàng)建epoll樹, 初始創(chuàng)建2000個子節(jié)點, 可擴容
int epfd = epoll_create(2000);
//初始化,根節(jié)點 關注的事件
struct epoll_event ev;
//讀寫事件
ev.events=EPOLLIN;
ev.data.fd=lfd;
//初始化epoll數(shù)根節(jié)點
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é)點 關注的事件
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ù)可設置,是否阻塞模式
//注意協(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;
}*/
//鍵盤讀入.默認為阻塞
read(STDIN_FILENO, buf, sizeof(buf));
//寫入到 根文件描述符
if(send(sockfd, buf, sizeof(buf), 0) < 0){
perror("send error!");
exit(1);
}
}
}