文件系统包含磁盘、文件格式以及与内核的交互。
inode维护了address_space模块,从而获得自身文件在内存中的缓存信息。 address_space内部维护了一个树结构指向文件读入内存所有的内存页。
struct address_space {
struct inode *host; /* Owner, either the inode or the block_device */
struct radix_tree_root page_tree; /* Cached pages */
spinlock_t tree_lock; /* page_tree lock */
struct prio_tree_root i_mmap; /* Tree of private and shared mappings */
struct spinlock_t i_mmap_lock; /* Protects @i_mmap */
unsigned long nrpages; /* total number of pages */
struct address_space_operations *a_ops; /* operations table */
...
}
内核使用task_struct来表示单个进程的描述符,维护进程的所有信息,其中包括files指针来指向结构体files_struct,files_struct中维护了文件描述符。
inode是文件的元信息,可以对应磁盘上的文件,也可以对应网络连接。IP+port是网络通信地址,而inode是文件系统提供给用户线程读写数据的方式。
struct socket_alloc {
struct socket socket;
struct inode vfs_inode;
};
注意:fd是进程表中open_files的下标,可以拿到file*,一个file可以是Inode、Pipe、Device、Socket等类型。
我们大概畅想下:
这就是socket函数返回后的内存结构体。后续我们调用bind,listen等等函数,传入fd,系统就会根据上面图的指向,一直找到tcp函数集,执行对应的函数,对于udp也是一样,不同是tcp函数集变成udp函数集。这一篇我们先介绍socket函数的逻辑,下面继续分析socket编程系列函数的实现。
不管客户端还是服务端都是先创建socket()。
// 新建一个socket结构体,并且创建一个下层的sock结构体,互相关联
static int sock_socket(int family, int type, int protocol)
{
int i, fd;
struct socket *sock;
struct proto_ops *ops;
// 找到对应的协议族,比如unix域、ipv4
for (i = 0; i < NPROTO; ++i)
{ // 从props数组中找到family协议对应的操作函数集,props由系统初始化时sock_register进行操作
if (pops[i] == NULL) continue;
if (pops[i]->family == family)
break;
}
if (i == NPROTO)
{
return -EINVAL;
}
// 函数集
ops = pops[i];
// 检查一下类型
if ((type != SOCK_STREAM && type != SOCK_DGRAM &&
type != SOCK_SEQPACKET && type != SOCK_RAW &&
type != SOCK_PACKET) || protocol < 0)
return(-EINVAL);
// 分配一个新的socket结构体
if (!(sock = sock_alloc()))
{
...
}
// 设置类型和操作函数集
sock->type = type;
sock->ops = ops;
if ((i = sock->ops->create(sock, protocol)) < 0)
{
sock_release(sock);
return(i);
}
// 返回一个新的文件描述符
if ((fd = get_fd(SOCK_INODE(sock))) < 0)
{
sock_release(sock);
return(-EINVAL);
}
return(fd);
}
struct socket {
short type; /* SOCK_STREAM, ... */
socket_state state;
long flags;
struct proto_ops *ops;
// 这个字段要记一下
void *data;
struct socket *conn;
struct socket *iconn;
struct socket *next;
struct wait_queue **wait;
struct inode *inode; //和socket相互引用
struct fasync_struct *fasync_list;
};
struct socket *sock_alloc(void)
{
struct inode * inode;
struct socket * sock;
// 获取一个可用的inode节点
inode = get_empty_inode();
if (!inode)
return NULL;
// 初始化某些字段
inode->i_mode = S_IFSOCK;
inode->i_sock = 1;// socket文件
inode->i_uid = current->uid;
inode->i_gid = current->gid;
// 指向inode的socket结构体,初始化inode结构体的socket结构体
sock = &inode->u.socket_i;
sock->state = SS_UNCONNECTED;
sock->flags = 0;
sock->ops = NULL;
sock->data = NULL;
sock->conn = NULL;
sock->iconn = NULL;
sock->next = NULL;
sock->wait = &inode->i_wait;
// 互相引用
sock->inode = inode; /* "backlink": we could use pointer arithmetic instead */
sock->fasync_list = NULL;
// socket数加一
sockets_in_use++;
// 返回新的socket结构体,他挂载在inode中
return sock;
}
// 创建一个sock结构体,和socket结构体互相关联
static int inet_create(struct socket *sock, int protocol)
{
struct sock *sk;
struct proto *prot;
int err;
// 分配一个sock结构体
sk = (struct sock *) kmalloc(sizeof(*sk), GFP_KERNEL);
switch(sock->type)
{
case SOCK_STREAM:
protocol = IPPROTO_TCP;
// 函数集
prot = &tcp_prot;
break;
case SOCK_DGRAM:
protocol = IPPROTO_UDP;
prot=&udp_prot;
break;
}
// sock结构体的socket字段指向上层的socket结构体
sk->socket = sock;
// 省略一堆对sock结构体的初始化代码
}
分配端口、修改为listen状态,设置接收队列长度
static int sock_listen(int fd, int backlog)
{
struct socket *sock;
if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL)
return(-EBADF);
if (!(sock = sockfd_lookup(fd, NULL)))
return(-ENOTSOCK);
if (sock->state != SS_UNCONNECTED)
{
return(-EINVAL);
}
if (sock->ops && sock->ops->listen)
sock->ops->listen(sock, backlog);
// 设置socket的监听属性,accept函数时用到
sock->flags |= SO_ACCEPTCON;
return(0);
}
static int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = (struct sock *) sock->data;
// 如果没有绑定端口则绑定一个,并把sock加到sock_array中
if(inet_autobind(sk)!=0)
return -EAGAIN;
if ((unsigned) backlog > 128)
backlog = 128;
// tcp接收队列的长度上限,不同系统实现不一样,具体参考tcp.c的使用
sk->max_ack_backlog = backlog;
// 修改socket状态,防止多次调用listen
if (sk->state != TCP_LISTEN)
{
sk->ack_backlog = 0;
sk->state = TCP_LISTEN;
}
return(0);
}
// 绑定一个随机的端口,更新sk的源端口字段,并把sk挂载到端口对应的队列中,见bind函数的分析
static int inet_autobind(struct sock *sk)
{
/* We may need to bind the socket. */
if (sk->num == 0)
{
sk->num = get_new_socknum(sk->prot, 0);
if (sk->num == 0)
return(-EAGAIN);
put_sock(sk->num, sk);
sk->dummy_th.source = ntohs(sk->num);
}
return 0;
}
tcp内部有一个哈希表保存所有socket,并没有分配实际的资源。每个监听socket有一个backlog,过载会丢包。
// 过载则丢包,防止ddos,max_ack_backlog即listen的参数
if (sk->ack_backlog >= sk->max_ack_backlog)
{
tcp_statistics.TcpAttemptFails++;
kfree_skb(skb, FREE_READ);
return;
}
server端遇到连接数量太多,无法打开新连接? 1. 一般是文件句柄数量太多,达到上限,并不是端口耗尽。 2. backlog接收队列已满,丢包
socket有两个队列:半连接队列、全连接队列,两者长度没有必然联系,半连接队列是在/proc/sys/net/ipv4/tcp_max_syn_backlog
,而全连接队列长度是在调用listen()函数时设置的。
如果客户端连接失败,有可能是半连接被打满,也有可能是全连接被打满。
半连接队列被打满可能是SYN Flood攻击,此时应该采用首包丢弃和源认证来解决。
SYN Flood攻击是生成无数个虚假地址来通信,一般地址会不断变化,不会应答的。
首包丢弃看是否能够超时重传,如果能,初步认为是正常的用户地址。
然后由Anti-DDoS系统代替服务器向客户端发送SYN-ACK报文,如果客户端不应答,则认为该客户端为虚假源;如果客户端应答,则Anti-DDoS系统认为该客户端为真实源,并将其IP地址加入白名单,在一段时间允许该源发送的所有SYN报文通过,也不做代答。
短连接的操作步骤是: 建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接 长连接的操作步骤是: 建立连接——数据传输…(保持连接)…数据传输——关闭连接
传输层保活机制
tcp具有保活功能,当tcp服务端回复之后会开启保活定时器,时间一到就会发送探测报文,重复10次后没有得到响应,则关闭连接。
以netty举例,通过IdleStateHandler来保活。
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// send heartbeat when read idle.
if (evt instanceof IdleStateEvent) {
try {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
if (logger.isDebugEnabled()) {
logger.debug("IdleStateEvent triggered, send heartbeat to channel " + channel);
}
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setEvent(HEARTBEAT_EVENT);
channel.send(req);//定时发送心跳
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
public class NettyServerHandler extends ChannelDuplexHandler {
private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);
/**
* the cache for alive worker channel.
* <ip:port, dubbo channel>
*/
private final Map<String, Channel> channels = new ConcurrentHashMap<>();
private final URL url;
private final ChannelHandler handler;
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// server will close channel when server don't receive any heartbeat from client util timeout.
if (evt instanceof IdleStateEvent) {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
logger.info("IdleStateEvent triggered, close channel " + channel);
//关闭
channel.close();
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
super.userEventTriggered(ctx, evt);
}
}
该类会开启心跳定时器,如果超时,会立刻注册一个IdleStateEvent
private void initialize(ChannelHandlerContext ctx) {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
switch (state) {
case 1:
case 2:
return;
default:
break;
}
state = 1;
initOutputChanged(ctx);
//开启定时器,
//客户端每过心跳间隔就立刻发送心跳。
//服务端定时扫描连接上次读写的时间,如果超时则关闭。
lastReadTime = lastWriteTime = ticksInNanos();
if (readerIdleTimeNanos > 0) {
readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (writerIdleTimeNanos > 0) {
writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (allIdleTimeNanos > 0) {
allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
长连接特点 1. 复用连接,可以减少连接创建和释放的开销,适用于客户端比较稳定的场景。 2. 会一直占用文件句柄,需要保活机制及时释放掉断连的连接。
短连接特点 1. 连接不会复用,每次请求都需要建立和拆除连接,性能较差,适用于客户端不稳定、请求频率较低的场景。 2. 很容易出现端口被占满,主动断开方会出现大量TIME_WAIT状态的tcp连接,只有等待2MSL才会关闭,如果服务端是主动断开连接,端口很快就会耗尽,可设置SO_RESUSEADDR来端口复用。
tcp保活机制在内核实现,不太适应应用层,不区分长连接和短连接。可能因为应用层导致无法及时响应请求,但连接还是正常的。
关闭连接时,被动断开方可能还有数据没传输完,不能立即断开连接,只能回复一个ACK响应主动断开方的FIN报文。而建立连接时,为了提高效率,被动方将ACK报文和自己的SYN报文合并成SYN+ACK报文,减少一次握手。
不能,第一次握手是主动方SYN请求,第二次握手是被动方的SYN+ACK请求,如果少了第三次握手,就无法对被动方的SYN报文进行确认,无法确保连接是否正常建立。四次握手是可以的,但是为了效率考虑,被动方将ACK报文和自己的SYN报文合并成SYN+ACK报文,减少一次握手。
一:被动断开方发送FIN报文后,主动断开方响应ACK报文,但是ACK报文可能会丢失,被动断开方无法顺利进入CLOSE状态,就会超时重传。
二:主动断开方需要等待2MSL,意味着端口要在2MSL后才能被新连接使用。2MSL时间后,旧连接所产生的报文已经从网络中消失了,确保新连接诶不会出现旧连接的报文。
TCP设有保活计时器,每收到一次client的数据帧后,server就会将保活计时器复位。计时器的超时时间一般设置为2h,若2h内没有收到client的数据帧,server就会发送探测报文,以后每隔75s发送一次,10次后没有响应,则认为client故障,关闭连接。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。