我正在参与「启航方案」
最近重温网络的时分,突然发现,底层就那么些接口,java必定也是封装了底层接口,看过我前面Nio相关的小伙伴必定知道对这些类有点影响(Buffer,Channel,Selector,SelectionKey),可是跟底层对应不起来啊,这一篇就透过源码看一下,大概能协助你更好的了解这几个类,及底层的完成。侧重个人兴趣向收拾,如有不适,欢迎吐槽
Linux网络编程
查阅资料的时分,发现wiki百科讲的现已十分好了,我先贴下原文Berkeley套接字,相当完美的描述了Socket相关Api介绍及demo演示,由于,大学学的c言语都快还给教师了,写个demo已然不太现实,这儿就臭不要脸的套用wiki百科的demo,c言语讲解部分,假如有错误欢迎指出~~。
下面是精简版别的linux网络编程,具体版能够参阅链接。
BIO
demo
/* Server code in C */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
struct sockaddr_in stSockAddr;
//TODO 1 socket
int SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(-1 == SocketFD)
{
perror("can not create socket");
exit(EXIT_FAILURE);
}
memset(&stSockAddr, 0, sizeof(struct sockaddr_in));
stSockAddr.sin_family = AF_INET;
stSockAddr.sin_port = htons(1100);
stSockAddr.sin_addr.s_addr = INADDR_ANY;
//TODO 2 bind
if(-1 == bind(SocketFD,(const struct sockaddr *)&stSockAddr, sizeof(struct sockaddr_in)))
{
perror("error bind failed");
close(SocketFD);
exit(EXIT_FAILURE);
}
//TODO 3 listen
if(-1 == listen(SocketFD, 10))
{
perror("error listen failed");
close(SocketFD);
exit(EXIT_FAILURE);
}
for(;;)
{
//TODO 4 accept
int ConnectFD = accept(SocketFD, NULL, NULL);
if(0 > ConnectFD)
{
perror("error accept failed");
close(SocketFD);
exit(EXIT_FAILURE);
}
/* perform read write operations ... */
shutdown(ConnectFD, SHUT_RDWR);
close(ConnectFD);
}
close(SocketFD);
return 0;
}
/* Client code in C */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
struct sockaddr_in stSockAddr;
int Res;
int SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (-1 == SocketFD)
{
perror("cannot create socket");
exit(EXIT_FAILURE);
}
memset(&stSockAddr, 0, sizeof(struct sockaddr_in));
stSockAddr.sin_family = AF_INET;
stSockAddr.sin_port = htons(1100);
Res = inet_pton(AF_INET, "192.168.1.3", &stSockAddr.sin_addr);
if (0 > Res)
{
perror("error: first parameter is not a valid address family");
close(SocketFD);
exit(EXIT_FAILURE);
}
else if (0 == Res)
{
perror("char string (second parameter does not contain valid ipaddress");
close(SocketFD);
exit(EXIT_FAILURE);
}
//TODO 5 connect
if (-1 == connect(SocketFD, (const struct sockaddr *)&stSockAddr, sizeof(struct sockaddr_in)))
{
perror("connect failed");
close(SocketFD);
exit(EXIT_FAILURE);
}
/* perform read write operations ... */
shutdown(SocketFD, SHUT_RDWR);
close(SocketFD);
return 0;
}
LinuxAPI
socket()
-
socket()
创立一个新的确认类型的套接字,回来套接字。- api:
int socket(int , int type, int protocol);
- 示例:上文 TODO 1
- 参数:
-
domain
: 为创立的套接字指定协议集 eg. IPV4 -
type
: socket类型 eg. 流,数据报文 -
protocol
: 实践传输协议 eg. TCP,UDP
-
- api:
bind()
-
bind()
为一个套接字分配地址。当运用socket()
创立套接字后,只赋予其所运用的协议,并未分配地址。在承受其它主机的衔接前,有必要先调用bind()为套接字分配一个地址。- api:
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
- 示例:拜见上文 TODO 2
- 参数:
-
sockfd
:套接字描述符,上面回来的套接字 -
my_addr
: 指向sockaddr结构(用于表明所分配地址)的指针 -
addrlen
: 用socklen_t字段指定了sockaddr结构的长度
-
- api:
listen()
-
listen()
当socket和一个地址绑定之后,listen()
函数会开端监听或许的衔接恳求。然而,这只能在有牢靠数据流确保的时分运用,例如:数据类型(SOCK_STREAM
,SOCK_SEQPACKET
)。- api:
int listen(int sockfd, int backlog);
- 示例:拜见上文 TODO 3
- 参数:
-
sockfd
: 套接字描述符,上面回来的套接字 -
backlog
: 完成三次握手、等候accept的全衔接的行列的最大长度上限。
-
- api:
accept()
-
accept()
当应用程序监听来自其他主机的面对数据流的衔接时,经过事情(比如Unix select()系统调用)通知它。有必要用accept()
函数初始化衔接。 Accept() 为每个衔接创立新的套接字并从监听行列中移除这个衔接。它运用如下参数:- api:
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
- 示例:拜见上文 TODO 4
- 参数:
-
sockfd
:监听的套接字描述符 -
cliaddr
: 指向sockaddr 结构体的指针,客户机地址信息。 -
addrlen
:指向socklen_t
的指针,确认客户机地址结构体的巨细 。
-
- api:
connect()
-
connect()
系统调用为一个套接字设置衔接,参数有文件描述符和主机地址。链接到指定地址- api:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
- 示例:拜见上文 TODO 5
- 参数:
-
sockfd
:监听的套接字描述符 -
serv_addr
: 指向sockaddr 结构体的指针,服务器地址信息。 -
addrlen
:指向socklen_t
的指针,确认服务器地址结构体的巨细 。
-
- api:
select()
-
select()
:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和反常等事情- api:
int select (int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
- 参数:
-
nfds
:没有用,仅仅为与伯克利Socket兼容而供给。 -
readfds
:指定一个Socket数组,select查看该数组中的一切Socket。假如成功回来,则readfds中寄存的是契合‘可读性’条件的数组成员(如缓冲区中有可读的数据)。 -
writefds
:指定一个Socket数组,select查看该数组中的一切Socket。假如成功回来,则writefds中寄存的是契合‘可写性’条件的数组成员(包含衔接成功)。 -
exceptfds
:指定一个Socket数组,select查看该数组中的一切Socket。假如成功回来,则cxceptfds中寄存的是契合‘有反常’条件的数组成员(包含衔接接失利)。 -
timeout
:指定select履行的最长时间,假如在timeout限制的时间内,readfds、writefds、exceptfds中指定的Socket没有一个契合要求,就回来0。
-
- api:
poll()
-
poll()
用于查看套接字的状况。 套接字能够被测验,看是否能够写入、读取或是有错误。- api:
int poll(struct pollfd * fds , nfds_t nfds , int timeout );
- 参数:
-
fds
是pollfd结构体指针 -
nfds
nfds是描述符个数,结构体pollfd数组元素的个数 -
timeout
:参数设置为-1时,表明永久堵塞等候。0表明当即回来,不堵塞。大于0时,表明等候指定数目的毫秒数。
-
- api:
NIO
demo
#include<stdio.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<ctype.h>
#define MAXLEN 1024
#define SERV_PORT 8000
#define MAX_OPEN_FD 1024
int main(int argc,char *argv[])
{
int listenfd,connfd,efd,ret;
char buf[MAXLEN];
struct sockaddr_in cliaddr,servaddr;
socklen_t clilen = sizeof(cliaddr);
struct epoll_event tep,ep[MAX_OPEN_FD];
listenfd = socket(AF_INET,SOCK_STREAM,0);
//TODO 6 fcntl
fcntl(listenfd, F_SETFL, fdflags | O_NONBLOCK);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,20);
// 创立一个epoll fd
//TODO 7 epoll_create
efd = epoll_create(MAX_OPEN_FD);
tep.events = EPOLLIN;tep.data.fd = listenfd;
// 把监听socket 先增加到efd中
//TODO 8 epoll_ctl
ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
// 循环等候
for (;;)
{
//TODO 9 epoll_wait
// 回来已安排妥当的epoll_event,-1表明堵塞,没有安排妥当的epoll_event,将一向等候
size_t nready = epoll_wait(efd,ep,MAX_OPEN_FD,-1);
for (int i = 0; i < nready; ++i)
{
// 假如是新的衔接,需要把新的socket增加到efd中
if (ep[i].data.fd == listenfd )
{
connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
tep.events = EPOLLIN;
tep.data.fd = connfd;
ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);
}
// 不然,读取数据
else
{
connfd = ep[i].data.fd;
int bytes = read(connfd,buf,MAXLEN);
// 客户端关闭衔接
if (bytes == 0){
ret =epoll_ctl(efd,EPOLL_CTL_DEL,connfd,NULL);
close(connfd);
printf("client[%d] closed\n", i);
}
else
{
for (int j = 0; j < bytes; ++j)
{
buf[j] = toupper(buf[j]);
}
// 向客户端发送数据
write(connfd,buf,bytes);
}
}
}
}
return 0;
}
fcntl()
-
fcntl()
打开文件描述符,具体操作由cmd决定- api:
int fcntl(int fd , int cmd , ... /* arg */ );
- 示例:拜见上文 TODO 6
- 参数
-
fd
:文件描述符 -
cmd
:操作指令
-
- api:
epoll_create()
-
epoll_create()
在内核中创立epoll
实例并回来一个epoll
文件描述符,结构体为eventpoll
。- api:
int epoll_create(int size);
- 示例:拜见上文 TODO 7
- 参数:
-
size
:而现在 size 现已没有这种语义了,可是调用者调用时 size 仍然有必要大于 0,以确保后向兼容性。
-
- api:
epoll_ctl()
-
epoll_ctl()
向 epfd 对应的内核epoll
实例增加、修正或删除对 fd 上事情 event 的监听。- api:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 示例:拜见上文 TODO 8
- 参数
-
epfd
epoll结构体 -
op
cud对应的事情枚举 -
fd
文件描述符 -
events
水平触发or边际触发
-
- api:
epoll_wait()
-
epoll_wait()
等候其办理的衔接上的 IO 事情- api:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 示例:拜见上文 TODO 9
- 参数
-
epfd
epoll结构体 -
events
epoll_event结构体指针 -
maxevents
最多回来多少事情 -
timeout
当 timeout 为 0 时,epoll_wait 永久会当即回来。而 timeout 为 -1 时,epoll_wait 会一向堵塞直到任一已注册的事情变为安排妥当。当 timeout 为一正整数时,epoll 会堵塞直到计时 timeout 毫秒终了或已注册的事情变为安排妥当。
-
- api:
send()和recv(),或许write()和read(),或许recvfrom()和sendto()等
- 用于往/从长途套接字发送和承受数据
相关api大致如上,如有更精细的能够自行查找。
其他延伸
linux内核能够看腾讯大佬:张彦⻜的博客,这儿就简述下,便利有个形象
- epoll_create
会创立如下结构体
struct eventpoll {
// sys_epoll_wait⽤到的等候行列
wait_queue_head_t wq;
//接纳安排妥当的描述符
struct list_head rdllist;
// 红⿊树,办理增加进来的socket(封装为epitem)
struct rb_root rbr;
};
- epoll_ctl
依据行为枚举的不同,进行不同的处理,对于EPOLL_CTL_ADD,会添封装成将socket封装成epitem,插入到红黑树中
- epoll_wait
查询安排妥当行列,安排妥当行列中没有,会增加进epoll目标的等候行列中,让出线程,当有数据进入,会讲等候行列中对应的项,增加到epoll的安排妥当行列,并唤醒线程,持续履行剩下代码
参阅文章
Berkeley套接字:zh.wikipedia.org/wiki/Berkel…
epoll:zh.wikipedia.org/wiki/Epoll
linux文档:man7.org/linux/man-p…