深入内核分析BindException异常原因

news2025/1/10 21:05:59

一、前言

前段时间公司内的站点发布时经常遇到Tomcat使用的8080端口被占用,导致启动报错BindException的情况。笔者参与了该问题的排查和修复,本文将深入TomcatOpenJDKLinux内核等源码为大家讲解问题的原因以及排查过程。

报错信息

Caused by: java.net.BindException: Address already in use

二、Tomcat源码分析

起初我们通过Debug发现公司内使用的SpringBoot 1.5.x 版本的内置Tomcat没有默认设置reuse参数(如下图,soReuseAddress默认为null)。根据网上的相关资料,如果没有设置SO_REUSEADDR参数,就可能会与TIME_WAIT状态的Socket发生端口冲突。但服务端需要设置SO_REUSEADDR参数是网络编程的基础知识,Tomcat应该不会犯这么低级的错误,所以让我们带着这个疑问,逐步深入阅读源码进行求证。

三、OpenJDK源码分析

通过上述错误堆栈一路追查到了 bind0 方法,该方法是 native 的在Java层面看不到源码,我们使用的JDK版本也不是开源的OpenJDK,但我们可以阅读OpenJDK的源码作为参考:

3.1bind0 函数源码

JNIEXPORT void JNICALL
Java_sun_nio_ch_Net_bind0(JNIEnv *env, jclass clazz, jobject fdo, jboolean preferIPv6,
                          
jboolean useExclBind, jobject iao, int port)
{
    
SOCKADDR sa;
    
int sa_len = SOCKADDR_LEN;
    
int rv = 0;

    
// 地址格式转换
    if (NET_InetAddressToSockaddr(env, iao, port, (struct sockaddr *)&sa, &sa_len, preferIPv6) != 0) {
      
return;
    }
    
// 1. 调用NET_Bind函数进行bind
    rv = NET_Bind(fdval(env, fdo), (struct sockaddr *)&sa, sa_len);
    
if (rv != 0) {
        
// 2. 处理异常
        handleSocketError(env, errno);
    }
}

该函数主要做了两件事:

  1. 调用NET_Bind函数执行bind操作。
  2. 调用handleSocketError函数处理bind操作返回的错误码。

3.2NET_Bind函数源码

int
NET_Bind(int fd, struct sockaddr *him, int len)
{

#if defined(__solaris__) && defined(AF_INET6)
    int level = -1;
    
int exclbind = -1;
#endif
    int rv;
    
int arg, alen;

#ifdef __linux__
    /*
     * ## get bugId for this issue - goes back to 1.2.2 port ##
     * ## When IPv6 is enabled this will be an IPv4-mapped
     * ## with family set to AF_INET6
     */
    if (him->sa_family == AF_INET) {
        
struct sockaddr_in *sa = (struct sockaddr_in *)him;
        
if ((ntohl(sa->sin_addr.s_addr) & 0x7f0000ff) == 0x7f0000ff) {
            
errno = EADDRNOTAVAIL;
            
return -1;
        }
    }

#endif

#if defined(__solaris__)
    /*
     * Solaris has separate IPv4 and IPv6 port spaces so we
     * use an exclusive bind when SO_REUSEADDR is not used to
     * give the illusion of a unified port space.
     * This also avoids problems with IPv6 sockets connecting
     * to IPv4 mapped addresses whereby the socket conversion
     * results in a late bind that fails because the
     * corresponding IPv4 port is in use.
     */
    alen = sizeof(arg);
    
if (useExclBind || getsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
                   (
char *)&arg, &alen) == 0) {
        
if (useExclBind || arg == 0) {
            
/*
             * SO_REUSEADDR is disabled or sun.net.useExclusiveBind
             * property is true so enable TCP_EXCLBIND or
             * UDP_EXCLBIND
             */
            alen = sizeof(arg);
            
if (getsockopt(fd, SOL_SOCKET, SO_TYPE, (char *)&arg,
                          
&alen) == 0) {
                
if (arg == SOCK_STREAM) {
                    
level = IPPROTO_TCP;
                    
exclbind = TCP_EXCLBIND;
                }
else {
                    
level = IPPROTO_UDP;
                    
exclbind = UDP_EXCLBIND;
                }
            }

            
arg = 1;
            
// 调用setsockopt系统调用,设置参数
            setsockopt(fd, level, exclbind, (char *)&arg,
                      
sizeof(arg));
            }
    }

#endif
    // 调用bind系统调用
    rv = bind(fd, him, len);

#if defined(__solaris__) && defined(AF_INET6)
    if (rv < 0) {
        
int en = errno;
        
/* Restore *_EXCLBIND if the bind fails */
        if (exclbind != -1) {
            
int arg = 0;
            
setsockopt(fd, level, exclbind, (char *)&arg,
                      
sizeof(arg));
        }
        
errno = en;
    }

#endif

    
return rv;
}

通过上述源码可以看出,JDK封装的bind函数并不是简单的对bind系统调用的封装,里面会根据各种判断逻辑在bind系统调用之前先执行setsockopt系统调用设置socket参数。所以Tomcat没有设置SO_REUSEADDR参数,是因为JDK默认设置了

3.3、基于strace工具实验验证

由于我们使用的不是OpenJDK,而上述结论是基于OpenJDK的源码得出的,不够严谨。所以我们又进行了如下验证:

  1. 编写一个简单的SpringBoot程序并打成Jar包。
  2. 基于线上相同的JDK版本使用如下命令启动。

sudo strace -f -e socket,bind,setsockopt,getsockopt,listen,connect,accept -o strace.out java -jar test.jar

strace命令会打印这个Java程序运行时发起的系统调用的参数和返回值,输出如下:

...
16340 socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 18
16340 setsockopt(18, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
16340 setsockopt(18, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
16340 bind(18, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
16340 listen(18, 100)                   = 0
16373 accept(18,  <unfinished ...>
...

可以看到在执行bind系统调用前,先执行了setsockopt系统调用,将SO_REUSEADDR设置为了true。所以端口冲突并不是与TIME_WAIT状态的连接发生的冲突

排查到这我们有了新的思路:只要找到抛出BindException异常的代码逻辑,就可以找到端口冲突的原因。

3.4handleSocketError函数源码

jint
handleSocketError(JNIEnv *env, jint errorValue)
{
    
char *xn;
    
switch (errorValue) {
        
case EINPROGRESS:       /* Non-blocking connect */
            return 0;
#ifdef EPROTO
        case EPROTO:
            
xn = JNU_JAVANETPKG "ProtocolException";
            
break;
#endif
        case ECONNREFUSED:
            
xn = JNU_JAVANETPKG "ConnectException";
            
break;
        
case ETIMEDOUT:
            
xn = JNU_JAVANETPKG "ConnectException";
            
break;
        
case EHOSTUNREACH:
            
xn = JNU_JAVANETPKG "NoRouteToHostException";
            
break;
        
case EADDRINUSE:  /* Fall through */
        case EADDRNOTAVAIL:
            
// 这里
            xn = JNU_JAVANETPKG "BindException";
            
break;
        
default:
            
xn = JNU_JAVANETPKG "SocketException";
            
break;
    }
    
errno = errorValue;
    
JNU_ThrowByNameWithLastError(env, xn, "NioSocketError");
    
return IOS_THROWN;
}

handleSocketError函数源码可以看到只有当内核的bind系统调用返回的错误码等于EADDRINUSE或者EADDRNOTAVAIL时才会抛出BindException。那么接下来我们就去深入内核源码,找到bind系统调用返回这两个错误码的逻辑。

四、Kernel(6.3.9版本)源码分析

4.1bind系统调用定义

// 定义了一个3个参数的系统调用 -- bind系统调用
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    
return __sys_bind(fd, umyaddr, addrlen);
}

// bind系统调用的代码逻辑入口
int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
    
struct socket *sock;
    
struct sockaddr_storage address;
    
int err, fput_needed;

    
// 1. 通过文件描述符fd,找到对应的socket对象
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    
if (sock) {
        
// 2. 内存copy,将传入的address信息从用户态内存copy到内核态
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        
if (!err) {
            
err = security_socket_bind(sock,
                           (
struct sockaddr *)&address,
                          
addrlen);
            
if (!err)
                
// 3. socket可能是网络socket或者unix socket,这里要到具体的实现类中查看内部实现逻辑
                err = sock->ops->bind(sock,
                              (
struct sockaddr *)
                              
&address, addrlen);
        }
        
fput_light(sock->file, fput_needed);
    }
    
return err;
}

  1. 通过文件描述符fd找到对应的socket对象。
  2. 将传入的address由用户态内存copy到内核态内存。
  3. 调用sock->ops->bind函数(inet_bind)。不同的协议有不同的bind实现。

4.2inet_bind函数

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
        
struct sock *sk = sock->sk;
        
u32 flags = BIND_WITH_LOCK;
        
int err;

        
/* If the socket has its own bind function then use it. (RAW) */
        if (sk->sk_prot->bind) {
                
return sk->sk_prot->bind(sk, uaddr, addr_len);
        }
        
if (addr_len < sizeof(struct sockaddr_in))
                
return -EINVAL;

        
/* BPF prog is run before any checks are done so that if the prog
         * changes context in a wrong way it will be caught.
         */
        err = BPF_CGROUP_RUN_PROG_INET_BIND_LOCK(sk, uaddr,
                                                
CGROUP_INET4_BIND, &flags);
        
if (err)
                
return err;
        
// 核心逻辑在__inet_bind函数中
        return __inet_bind(sk, uaddr, addr_len, flags);
}

EXPORT_SYMBOL(inet_bind);

int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len,
                
u32 flags)
{
        
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
        
struct inet_sock *inet = inet_sk(sk);
        
struct net *net = sock_net(sk);
        
unsigned short snum;
        
int chk_addr_ret;
        
u32 tb_id = RT_TABLE_LOCAL;
        
int err;

        
if (addr->sin_family != AF_INET) {
                
/* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)
                 * only if s_addr is INADDR_ANY.
                 */
                err = -EAFNOSUPPORT;
                
if (addr->sin_family != AF_UNSPEC ||
                    addr->sin_addr.s_addr != htonl(INADDR_ANY))
                        
goto out;
        }

        
tb_id = l3mdev_fib_table_by_index(net, sk->sk_bound_dev_if) ? : tb_id;
        
chk_addr_ret = inet_addr_type_table(net, addr->sin_addr.s_addr, tb_id);

        
/* Not specified by any standard per-se, however it breaks too
         * many applications when removed.  It is unfortunate since
         * allowing applications to make a non-local bind solves
         * several problems with systems using dynamic addressing.
         * (ie. your servers still start up even if your ISDN link
         *  is temporarily down)
         */
        // EADDRNOTAVAIL错误码,当内核返回这个错误码时JDK会抛出BindException。当inet_addr_valid_or_nonlocal函数返回false时则会直接返回这个错误码
        err = -EADDRNOTAVAIL;
        
if (!inet_addr_valid_or_nonlocal(net, inet, addr->sin_addr.s_addr,
                                        
chk_addr_ret))
                
goto out;
        
// socket的源端口(8080
        snum = ntohs(addr->sin_port);
        
err = -EACCES;
        
if (!(flags & BIND_NO_CAP_NET_BIND_SERVICE) &&
            snum && inet_port_requires_bind_service(net, snum) &&
            !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
                
goto out;

        
/*      We keep a pair of addresses. rcv_saddr is the one
         *      used by hash lookups, and saddr is used for transmit.
         *
         *      In the BSD API these are the same except where it
         *      would be illegal to use them (multicast/broadcast) in
         *      which case the sending device address is used.
         */
        if (flags & BIND_WITH_LOCK)
                
lock_sock(sk);

        
/* Check these errors (active socket, double bind). */
        err = -EINVAL;
        
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
                
goto out_release_sock;

        
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
        
if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
                
inet->inet_saddr = 0;  /* Use device */

        
// 服务端的源端口是主动设置上去的不是随机的所以这里源端口存在,所以会进入这个if
        /* Make sure we are allowed to bind here. */
        if (snum || !(inet->bind_address_no_port ||
                      (flags & BIND_FORCE_ADDRESS_NO_PORT))) {
                
// 这里会校验源端口是否冲突,需要看这个方法内部会不会返回那两个特殊的错误码
                err = sk->sk_prot->get_port(sk, snum);
                
if (err) {
                        
inet->inet_saddr = inet->inet_rcv_saddr = 0;
                        
goto out_release_sock;
                }
                
if (!(flags & BIND_FROM_BPF)) {
                        
err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
                        
if (err) {
                                
inet->inet_saddr = inet->inet_rcv_saddr = 0;
                                
if (sk->sk_prot->put_port)
                                        
sk->sk_prot->put_port(sk);
                                
goto out_release_sock;
                        }
                }
        }

        
if (inet->inet_rcv_saddr)
                
sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
        
if (snum)
                
sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
        
inet->inet_sport = htons(inet->inet_num);
        
inet->inet_daddr = 0;
        
inet->inet_dport = 0;
        
sk_dst_reset(sk);
        
err = 0;
out_release_sock:
        
if (flags & BIND_WITH_LOCK)
                
release_sock(sk);
out:
        
return err;
}

__inet_bind函数中我们遇到了EADDRNOTAVAIL,当inet_addr_valid_or_nonlocal函数返回false时就会返回这个错误码。下面来看下这个函数的源码,看下在这个场景中他是否会返回false。除此之外,sk->sk_prot->get_port函数用于校验端口号是否可用,该函数也可能会返回EADDRINUSEEADDRNOTAVAIL错误码,需要重点排查。

4.3inet_addr_valid_or_nonlocal函数源码

static inline bool inet_addr_valid_or_nonlocal(struct net *net,
                                               struct inet_sock *inet,
                                               __be32 addr,
                                               int addr_type)
{
        return inet_can_nonlocal_bind(net, inet) ||
                //
我们是服务端的socketaddr默认等于 INADDR_ANY,这条判断只会为true
                addr == htonl(INADDR_ANY) ||
                addr_type == RTN_LOCAL ||
                addr_type == RTN_MULTICAST ||
                addr_type == RTN_BROADCAST;
}

该函数是由5个条件的或运算表达式组成的,在我们的场景中addr == htonl(INADDR_ANY)必定为true,所以该函数在我们的场景中只会返回true不会返回false。如果后面没有其他地方返回EADDRNOTAVAIL错误码,那么可以判断BindException不是因为内核返回了EADDRNOTAVAIL错误码导致的 接下来继续看sk->sk_prot->get_port函数(inet_csk_get_port)的逻辑。

4.4inet_csk_get_port函数

int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
    struct inet_hashinfo *hinfo = tcp_or_dccp_get_hashinfo(sk);
    //
由于我们之前设置过SO_REUSEADDR参数,并且此时还没有调用listen系统调用socket状态不等于TCP_LISTEN,所以reusetrue
    bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
    bool found_port = false, check_bind_conflict = true;
    bool bhash_created = false, bhash2_created = false;
    //
导致BindException的错误码出现了!
    int ret = -EADDRINUSE, port = snum, l3mdev;
    struct inet_bind_hashbucket *head, *head2;
    struct inet_bind2_bucket *tb2 = NULL;
    struct inet_bind_bucket *tb = NULL;
    bool head2_lock_acquired = false;
    struct net *net = sock_net(sk);

    l3mdev = inet_sk_bound_l3mdev(sk);
    //
如果没有指定端口,会进入这里随机端口。服务端socket不会进入这个if
    if (!port) {
        head = inet_csk_find_open_port(sk, &tb, &tb2, &head2, &port);
        if (!head)
            return ret;

        head2_lock_acquired = true;

        if (tb && tb2)
            goto success;
        found_port = true;
    } else {
        head = &hinfo->bhash[inet_bhashfn(net, port,
                          hinfo->bhash_size)];
        spin_lock_bh(&head->lock);
        inet_bind_bucket_for_each(tb, &head->chain)
            if (inet_bind_bucket_match(tb, net, port, l3mdev))
                break;
    }

    if (!tb) {
        tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net,
                         head, port, l3mdev);
        if (!tb)
            // ①
inet_bind_bucket_create函数返回为空时会返回EADDRINUSE错误码
            goto fail_unlock;
        bhash_created = true;
    }

    if (!found_port) {
        if (!hlist_empty(&tb->owners)) {
            if (sk->sk_reuse == SK_FORCE_REUSE ||
                (tb->fastreuse > 0 && reuse) ||
                sk_reuseport_match(tb, sk))
                check_bind_conflict = false;
        }
        // ②
inet_use_bhash2_on_bind函数和inet_bhash2_addr_any_conflict函数都返回true会返回EADDRINUSE错误码
        if (check_bind_conflict && inet_use_bhash2_on_bind(sk)) {
            if (inet_bhash2_addr_any_conflict(sk, port, l3mdev, true, true))
                goto fail_unlock;
        }

        head2 = inet_bhashfn_portaddr(hinfo, sk, net, port);
        spin_lock(&head2->lock);
        head2_lock_acquired = true;
        tb2 = inet_bind2_bucket_find(head2, net, port, l3mdev, sk);
    }

    if (!tb2) {
        tb2 = inet_bind2_bucket_create(hinfo->bind2_bucket_cachep,
                           net, head2, port, l3mdev, sk);
        if (!tb2)
            //
相同,当inet_bind2_bucket_create返回空时返回EADDRINUSE错误码
            goto fail_unlock;
        bhash2_created = true;
    }

    if (!found_port && check_bind_conflict) {
        // ③
inet_csk_bind_conflict返回true时,返回EADDRINUSE错误码
        if (inet_csk_bind_conflict(sk, tb, tb2, true, true))
            goto fail_unlock;
    }

success:
    inet_csk_update_fastreuse(tb, sk);

    if (!inet_csk(sk)->icsk_bind_hash)
        inet_bind_hash(sk, tb, tb2, port);
    WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
    WARN_ON(inet_csk(sk)->icsk_bind2_hash != tb2);
    ret = 0;

fail_unlock:
    if (ret) {
        if (bhash_created)
            inet_bind_bucket_destroy(hinfo->bind_bucket_cachep, tb);
        if (bhash2_created)
            inet_bind2_bucket_destroy(hinfo->bind2_bucket_cachep,
                          tb2);
    }
    if (head2_lock_acquired)
        spin_unlock(&head2->lock);
    spin_unlock_bh(&head->lock);
    return ret;
}

inet_csk_get_port函数源码中有3处可能会返回EADDRINUSE错误码的逻辑,下面会逐个分析。

  1. 当inet_bind_bucket_create函数返回为空时会返回EADDRINUSE错误码
  • inet_bind_bucket_create函数源码

struct inet_bind_bucket *inet_bind_bucket_create(struct kmem_cache *cachep,
                         struct net *net,
                         struct inet_bind_hashbucket *head,
                         const unsigned short snum,
                         int l3mdev)
{
    //
分配内存
    struct inet_bind_bucket *tb = kmem_cache_alloc(cachep, GFP_ATOMIC);
    //
如果内存分配成功则初始化,否则直接返回空
    if (tb) {
        write_pnet(&tb->ib_net, net);
        tb->l3mdev    = l3mdev;
        tb->port      = snum;
        tb->fastreuse = 0;
        tb->fastreuseport = 0;
        INIT_HLIST_HEAD(&tb->owners);
        hlist_add_head(&tb->node, &head->chain);
    }
    return tb;
}

inet_bind_bucket_create函数源码中可以看出,只有当内存分配失败时才可能返回空,这种情况还是比较少见的基本可以排除这个原因。

  1. 当inet_use_bhash2_on_bind函数和inet_bhash2_addr_any_conflict函数都返回true会返回EADDRINUSE错误码
  • inet_use_bhash2_on_bind函数源码

static bool inet_use_bhash2_on_bind(const struct sock *sk)
{
#if IS_ENABLED(CONFIG_IPV6)
    if (sk->sk_family == AF_INET6) {
        int addr_type = ipv6_addr_type(&sk->sk_v6_rcv_saddr);

        return addr_type != IPV6_ADDR_ANY &&
            addr_type != IPV6_ADDR_MAPPED;
    }
#endif
    //
服务端socket的源地址默认等于INADDR_ANY,所以这里只会返回false
    return sk->sk_rcv_saddr != htonl(INADDR_ANY);
}

inet_use_bhash2_on_bind函数在服务端socket的场景下只会返回false,所以排除这个可能性。

  1. 当inet_csk_bind_conflict返回true时,返回EADDRINUSE错误码

只剩这最后一种可能性了,从网上的资料可知inet_csk_bind_conflict函数是用来判断端口是否冲突的,当我们设置了SO_REUSEADDR参数之后,该函数还是返回端口冲突,那么就只能是在Tomcat启动之前有其他的Socket占用了8080端口导致的。 由于问题并不是必现的,所以一定不是其他的服务端socket也占用了8080端口导致的,只能是因为某些组件客户端在Tomcat启动之前进行初始化,发起了网络请求随机到了8080端口导致的(比如配置中心、MQ等都会在初始化时连接服务端)。在通过sysctl看了下系统配置,net.ipv4.ip_local_port_range的值被设置为了1024~65535,看到这里基本就可以确定是由于随机端口的范围设置不合理导致的,调整为10000~65535后问题得到解决。

五、总结

BindException是一个比较常见的异常,可能大多数程序员都遇到过,在遇到该问题时使用万能的重启大法基本都可以解决,所以也很少有人去排查定位该问题的根因。本文基于TomcatOpenJDKLinux内核等源码由浅入深的分析了该异常可能出现的原因,希望对大家有所帮助。

作者简介

LGW 信也科技基础架构研发专家,主要负责分布式对象存储的研发工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2131641.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

收到了大厂中秋礼盒,哪家赢了?

大家好&#xff0c;我是鸭鸭&#xff01; 中秋节越来越近啦&#xff0c;大家都收到放假通知和中秋月饼了吗&#xff1f; 各大互联网品牌大厂的中秋创意礼盒也来啦&#xff01; 字节 今年字节的中秋礼盒&#xff0c;除了广州酒家的月饼之外&#xff0c;还发了一床2m*2.3m的四…

INDEMIND:扫地机器人,仍然不够“香”

不仅需要“新花样”&#xff0c;还要搞好“基本功”。 行业祛魅&#xff0c;重啃技术战 正如所有人都知道市场会发生变化&#xff0c;但扫地机器人的陡然降温还是给大多数人上了一课。尽管到了2023年&#xff0c;市场有所复苏&#xff0c;但零售量的增长也仅为4%。一时间&…

医药|基于springboot的医药管理系统设计与实现(附项目源码+论文+数据库)

私信或留言即免费送开题报告和任务书&#xff08;可指定任意题目&#xff09; 目录 一、摘要 二、相关技术 三、系统设计 四、数据库设计 五、核心代码 六、论文参考 七、源码获取 一、摘要 计算机网络发展到现在已经好几十年了&#xff0c;在理论上面已…

基于vue框架的宠物管理平台的设计与实现f3193(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,医院简介,养宠知识,宠物分类,医生,预约医生,医嘱记录,宠物用品,用品分类,购买记录,供应商,宠物信息 开题报告内容 基于Vue框架的宠物管理平台的设计与实现开题报告 一、引言 随着宠物经济的兴起&#xff0c;宠物管理成为了一个日…

Win11+Ubuntu20.04双系统安装教程(避坑版)

Win11Ubuntu20.04双系统安装教程&#xff08;避坑版&#xff09; 前言系统盘制作安装Rufus系统盘制作 Windows磁盘配置移动分区&#xff08;磁盘分区时出现不连续的未分配空间需要用到&#xff0c;如果是连续的未分配空间即无需操作&#xff09;安装分区助手移动分区 安装Ubunt…

Redis的IO模型

Redis IO模型 Redis IO模型 使用的是基于 Reactor 模式的 I/O 多路复用模型。这个模型通过单线程事件循环来处理所有的客户端请求和响应。 基本模式 1. Reactor 模式 Reactor 模式是一种用于处理并发 I/O 操作的设计模式。它包含以下几个组件&#xff1a; 多路复用器&…

构建高效入学审核系统:Spring Boot解决方案

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理大学生入学审核系统的相关信息成为必然。开…

redis常见的数据类型?

参考&#xff1a;一文读懂Redis五种数据类型及应用场景 - 知乎 (zhihu.com) String 类型 String 类型&#xff1a;Redis 最基本的数据类型&#xff0c;它是二进制安全的&#xff0c;意味着你可以用它来存储任何类型的数据&#xff0c;如图片、序列化对象等。使用场景&#xff…

OceanBase 运维管理工具 OCP 4.x 升级:聚焦高可用、易用性及可观测性

可视化的管控平台&#xff0c;对 OceanBase 这类的分布式数据库及大规模数据的运维管理来说&#xff0c;是提升运维效率与数据库管理水平的重要工具。OceanBase 运维管理工具 OCP 作为专为OceanBase数据库设计的企业级全生命周期管理平台&#xff0c;为用户提供了全面的数据库可…

句子成分——每日一划(六)

顺手简答一划&#xff1a;And&#xff1a;连词 you&#xff1a;主语 my friend&#xff1a;插入语 you&#xff1a;对主语起强调作用 are&#xff1a;系动词 the real hero&#xff1a;表语 目录 一、原句 二、独立成分&#xff0c;状语(Adverbial Phrase) 三、条件状语从…

Leetcode面试经典150题-82.删除排序链表中的重复元素II

之前写过这个题的基础第83题&#xff0c;看本文之前一定要先看懂这个Leetcode面试经典150题-82.删除排序链表中的重复元素II前序-83.删除排序链表中的重复元素_删除链表中重复的元素-CSDN博客 直接上代码了&#xff0c;解法都在代码里&#xff0c;不懂就留言或者私信 /*** De…

电机驱动开发之驱动板

目录 1.主要器件选型2.原理图设计3.PCB绘制电源调理驱动电路电流反馈位置反馈 4.PCB绘制5.打板验证6.总结 1.主要器件选型 器件参数封装理由LDOLM317DCYR &#xff08;24V-12V 12V-5V&#xff09;SOT-223小电流应用 LDO比DCDC噪声小响应快更为稳定预驱FD6288TTssop-20常见无刷…

独立站新纪元:破局而出,共绘可持续发展蓝图

随着全球电商市场的日益繁荣与平台竞争的加剧,独立站作为商家自主掌控品牌与市场的桥头堡,正面临着前所未有的挑战与机遇。在这个瞬息万变的时代,如何在平台垄断的阴影下突围而出,实现可持续增长,成为了每一位独立站商家亟需解答的课题。为此,店匠科技( Shoplazza ) 将于 9月 2…

基于SpringBoot+Vue的高校竞赛管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的…

2024年程序员接单平台汇总,程序员偷偷赚钱的机会来了!

作为技术创新的推动者和代码的创造者&#xff0c;程序员的影响力早已跨越国界。来到2024年&#xff0c;程序员不仅着眼于眼前的一亩三分地&#xff0c;也慢慢将眼光投向了外包接单。 程序员外包接单作为程序员副业的一种常见形式&#xff0c;给程序员带来了更多的选择&#xf…

C盘维护和清理心得(磁盘清理亲身实践)

01 安装软件前要自定义 安装软件都是默认C盘 通常可以选择安装储存位置至于应用数据会自动存在C盘&#xff0c;但一般并不大 02 安装后自定义存储位置 各种储存位置当然也默认C盘&#xff0c;通常分为下载位置和缓存位置 最经典的微信&#xff0c;QQ&#xff0c;钉钉等社交…

后台数据库查询记录

一、根据日期按天分组查询倒序 //mapper public List<Date> dateByPatientId(FollowScheme followScheme); <select id"dateByPatientId" parameterType"com.ruoyi.follow.domain.FollowScheme" resultType"java.util.Date">SELECT…

Redis:发布(pub)与订阅(sub)实战

前言 Redis发布订阅&#xff08;Pub/Sub&#xff09;是Redis提供的一种消息传递机制&#xff0c;它使用“发布者-订阅者”&#xff08;publisher-subscriber&#xff09;模式来处理消息传递。在这种模式下&#xff0c;发布者将消息发布到一组订阅者中&#xff0c;而无需关心谁…

C++八股总结(不间断更新)

数据类型和大小&#xff08;32位和64位&#xff09; char&#xff1a;1字节 1字节 short&#xff1a;2字节 2字节 int&#xff1a;4字节 4字节 long&#xff1a;4字节 8字节 long long&#xff1a;8字节 8字节 new-delete malloc-free new是C中的关键字。new可以根据动态分配内…

中等职业学校新媒体一键分发软件实训室解决方案

一、产品介绍 新媒体一键分发软件实训平台是专为中等职业学校设计&#xff0c;以满足新媒体运营、营销等岗位需求的教育解决方案。该平台通过模拟真实的新媒体工作环境&#xff0c;提供账号管理、内容编辑、一键分发等功能&#xff0c;使学生能够在实际操作中掌握新媒体技术的…