主頁 > 軟體設計 > TCP粘包處理現象及其解決方案——基于NewLife.Net網路庫的管道式幀長粘包處理方法

TCP粘包處理現象及其解決方案——基于NewLife.Net網路庫的管道式幀長粘包處理方法

2020-09-11 15:10:13 軟體設計

目錄

  • 1.粘包現象
  • 2.粘包原因
    • 2.1. 同一客戶端連續發送
    • 2.2. 網路擁塞造成粘包
    • 2.3. 服務端卡死了
  • 3. 粘包的危害
    • 3.1. 無法正確決議資料包
    • 3.2. 錯誤資料包被錯誤決議
    • 3.3. 進入死回圈
  • 4. 粘包的邏輯處理方式
    • 4.1. 根據包尾特征引數進行區分
    • 4.2. 根據包頭包尾特征引數進行區分
    • 4.3. 根據報文長度來進行粘包區分
  • 5. 根據報文長度來區分粘包的代碼落地——基于NewLife.Net的管道處理
    • 5.1. NewLife.Net管道架構處理方式
    • 5.2. 跟http的管道類比
    • 5.3.拆分粘包解碼器(根據長度解碼)
      • 5.3.1. 長度偏移地址Offset屬性
      • 5.3.2.長度位元組數Size屬性
      • 5.3.3. 編碼方法Encode
      • 5.3.4. 解碼方法Decode
        • 5.3.4.1.解碼步驟1——實體化長度解碼器物件
        • 5.3.4.2.解碼步驟2——將解碼前的報文列印
        • 5.3.4.3.解碼步驟3——將報文進行解碼
        • 5.3.4.4.將粘包處理結果進行列印
      • 5.3.5.清空粘包編碼器
      • 5.3.6.完整拆分粘包解碼器代碼
  • 6.長度計算委托GetLength
  • 7.最終粘包拆分效果圖

1.粘包現象

每個TCP 長連接都有自己的socket快取buffer,默認大小是8K,可支持手動設定,粘包是TCP長連接中最常見的現象,如下圖

socket快取中有5幀(或者說5包)心跳資料,包頭即F0 AA 55 0F(十六進制),通過數包頭資料我們確認出來快取里有5幀心跳包,但是5幀資料彼此頭尾相連粘合在了一起,這種常見的TCP快取現象,我們稱之為粘包,

2.粘包原因

2.1. 同一客戶端連續發送

同一客戶端連續發送心跳資料,當TCP服務端還來不及決議(如果決議完會把快取清掉),造成了同一快取資料包的粘合,

2.2. 網路擁塞造成粘包

當某一時刻發生了網路擁塞,一會之后,突然網路暢通,TCP服務端收到同一客戶端的多個心跳包,多個資料包會在TCP服務端的快取中進行了粘合,

2.3. 服務端卡死了

當服務端因為計算量過大或者其他的原因,計算緩慢,來不及處理TCP Socket快取中的資料,多個心跳包(或者其他報文)也會在socket快取中首尾相連,粘包,

總而言之,就是多個資料包在同一個TCP socket快取中進行了首尾相連現象,即為粘包現象,

3. 粘包的危害

由于粘包現象存在的客觀性,我們必須人為地在程式邏輯里將其區分,如果不去區分,任由各個資料包進行粘連,有以下幾點危害:

3.1. 無法正確決議資料包

服務端會不斷識別為無效包,告訴客戶端,客戶端會再次上報,因此會增加客戶端服務端的運行壓力,如果本身運算量很大,則會出現一些例外奔潰現象,

3.2. 錯誤資料包被錯誤決議

無巧不成書,如果錯誤的粘包,湊巧被服務端進行成功決議,則會進行錯誤的Handler 處理,這樣的錯誤處理方式危害會超過3.1,

3.3. 進入死回圈

如果頻率過快,則會出現這種現象,服務器不斷識別粘包為無效包,客戶端不斷上報,以此消耗CPU的占用率,

綜上,我們必須要進行TCP的粘包處理,這是軟體系統健壯性跟例外處理機制的基礎,

4. 粘包的邏輯處理方式

4.1. 根據包尾特征引數進行區分

規定幾個位元組為每幀TCP報文的包尾特征(比如4個位元組),檢索整個socket快取位元組,每當檢測到包尾特征位元組的時候,就劃分報文,以此來正確分割粘包,
特征:需要檢測每個位元組,效率較低,適合短報文,如果報文很長則不適合,

4.2. 根據包頭包尾特征引數進行區分

與4.1相似,多了包頭檢測部分,
特征:只需檢測第一幀的每個位元組,第二幀只需檢測包頭部分,適合長報文

4.3. 根據報文長度來進行粘包區分

根據報文長度偏置值,讀第一幀的報文,從粘包中(socket快取)劃分出第一幀正確報文,找第二幀的報文長度,劃分第二幀,以此劃分到底,
舉例:如下長度偏置為5(從0開始計算),即第6,第7位元組為報文長度位元組,

特征:只需檢測報文長度部分,適合長短報文的粘包劃分,

5. 根據報文長度來區分粘包的代碼落地——基于NewLife.Net的管道處理

5.1. NewLife.Net管道架構處理方式

Newlife.Net管道架構的設計,參考了java的Netty開源框架,因此大部分Netty的編解碼器都可以在此使用,
具體在代碼中的表現為

 _pemsServer.Add(new StickPackageSplit { Size = 2 });

即將LengthCodec這個編解碼器加入到了管道中去,所有的message都會經過LengthCodec這里主要是解碼功能,沒有進行編碼,解碼成功后(粘包根據長度劃分出多個有效包)推送到OnReceive方法中去,Size = 2表示報文長度是2個位元組,

5.2. 跟http的管道類比

與Net Core 的WEBAPI專案的管道添加,是否發現似曾相識?

  app.UseAuthentication();
  app.UseRequestLog();
  app.UseCors(_defaultCorsPolicyName);
  app.UseMvc();

管道添加的先后順序即資料流流經管道的順序,只是沒去追求是先有socket的管道處理機制,還是http 背景關系的管道處理機制,但是道理是相同的,

5.3.拆分粘包解碼器(根據長度解碼)

5.3.1. 長度偏移地址Offset屬性

長度所在位置的偏移地址,默認為5,解釋詳見4.3,

        //
        // 摘要:
        //     長度所在位置
        public int Offset
        {
            get;
            set;
        } = 5;

5.3.2.長度位元組數Size屬性

本文討論長度位元組數為2,詳見4.3

        //
        // 摘要:
        //     長度占據位元組數,1/2/4個位元組,0表示壓縮編碼整數,默認2
        public int Size
        {
            get;
            set;
        } = 2;

5.3.3. 編碼方法Encode

        //
        // 摘要:
        //     編碼,此應用不需要編碼,只需解碼,
        //     按長度將粘包劃分成多個資料包
        //
        // 引數:
        //   context:
        //
        //   msg:
        protected override object Encode(IHandlerContext context, Packet msg)
       { 
           return msg;
       }

這里無需編碼,故直接回傳msg,

5.3.4. 解碼方法Decode

        //
        // 摘要:
        //     解碼
        //
        // 引數:
        //   context:
        //
        //   pk:
        protected override IList<Packet> Decode(IHandlerContext context, Packet pk)
        {
            IExtend extend = context.Owner as IExtend;

            LengthCodec packetCodec = extend["Codec"] as LengthCodec;
           
            if (packetCodec == null)
            {
                IExtend extend2 = extend;
                LengthCodec obj = new LengthCodec
                {
                    Expire = Expire,
                    GetLength = ((Packet p) => MessageCodec<Packet>.GetLength(p, Offset, Size))
                };
                packetCodec = obj;
                extend2["Codec"] = obj;
            }
            
            Console.WriteLine("報文解碼前:{0}", BitConverter.ToString(pk.ToArray()));
            IList<Packet> list = packetCodec.Parse(pk);
            Console.WriteLine("報文解碼");
            foreach (var item in list)
            {
                Console.WriteLine("粘包處理結果:{0}", BitConverter.ToString(item.ToArray()));
            }

            return list;
        }

5.3.4.1.解碼步驟1——實體化長度解碼器物件

實體化長度解碼器完成之后,并將其添加到字典中去,

    IExtend extend2 = extend;
    LengthCodec obj = new LengthCodec
    {
        Expire = Expire,
        GetLength = ((Packet p) => MessageCodec<Packet>.GetLength(p, Offset, Size))
    };
    packetCodec = obj;
    extend2["Codec"] = obj;

5.3.4.2.解碼步驟2——將解碼前的報文列印

此步驟非必須,為了最后能讓讀者看到效果增加,

    Console.WriteLine("報文解碼前:{0}", BitConverteToString(pk.ToArray()));

5.3.4.3.解碼步驟3——將報文進行解碼

 IList<Packet> list = packetCodec.Parse(pk);

解碼代碼如下:

        //
        // 摘要:
        //     分析資料流,得到一幀資料
        //
        // 引數:
        //   pk:
        //     待分析資料包
        public virtual IList<Packet> Parse(Packet pk)
        {
            MemoryStream stream = Stream;
            bool num = stream == null || stream.Position < 0 || stream.Position >= stream.Length;
            List<Packet> list = new List<Packet>();


            if (num)
            {

                if (pk == null)
                {
                    return list.ToArray();
                }
                int i;
                int num2;

                for (i = 0; i < pk.Total; i += num2)
                {
                    Packet packet = pk.Slice(i);

                    num2 = GetLength(packet);

                    Console.WriteLine(" pk. GetLength(packet):{0}", num2);

                    if (num2 <= 0 || num2 > packet.Total)
                    {
                        break;
                    }
                    packet.Set(packet.Data, packet.Offset, num2);
                    list.Add(packet);
                }


                if (i == pk.Total)
                {
                  
                    return list.ToArray();
                }
                pk = pk.Slice(i);
            }

            lock (this)
            {
                CheckCache();
                stream = Stream;
                if (pk != null && pk.Total > 0)
                {
                    long position = stream.Position;
                    stream.Position = stream.Length;
                    pk.CopyTo(stream);
                    stream.Position = position;
                }
                while (stream.Position < stream.Length)
                {
                    Packet packet2 = new Packet(stream);
                    int num3 = GetLength(packet2);
                    if (num3 <= 0 || num3 > packet2.Total)
                    {
                        break;
                    }
                    packet2.Set(packet2.Data, packet2.Offset, num3);
                    list.Add(packet2);
                    stream.Seek(num3, SeekOrigin.Current);
                }
                if (stream.Position >= stream.Length)
                {
                    stream.SetLength(0L);
                    stream.Position = 0L;
                }


                return list;
            }
        }

解碼核心代碼如下:
即獲得每幀報文的長度,通過委托方法 GetLength(packet),然后回圈所有粘包報文,根據每幀報文的長度分割保存到list中去,最后回傳list,list的每個元素會觸發message接收事件,

委托的使用請敬請關注下一篇,委托代碼詳見6.

    for (i = 0; i < pk.Total; i += num2)
    {
        Packet packet = pk.Slice(i);

        num2 = GetLength(packet);

        Console.WriteLine(" pk. GetLength(packet):{0}", num2);

        if (num2 <= 0 || num2 > packet.Total)
        {
            break;
        }
        packet.Set(packet.Data, packet.Offset, num2);
        list.Add(packet);
    }

5.3.4.4.將粘包處理結果進行列印

    foreach (var item in list)
    {
        Console.WriteLine("粘包處理結果:{0}"BitConverter.ToString(item.ToArray()));
    }

5.3.5.清空粘包編碼器

該方法由NewLife.Net網路庫呼叫,我們無需關心,

    //
    // 摘要:
    //     連接關閉時,清空粘包編碼器
    //
    // 引數:
    //   context:
    //
    //   reason:
    public override bool Close(IHandlerContext contextstring reason)
    {
        IExtend extend = context.Owner as IExtend;
        if (extend != null)
        {
            extend["Codec"] = null;
        }
        return base.Close(context, reason);
    }

5.3.6.完整拆分粘包解碼器代碼

    // 摘要:
    //     長度欄位作為頭部
    // 
    public class StickPackageSplit : MessageCodec<Packet>
    {
        //
        // 摘要:
        //     長度所在位置
        public int Offset
        {
            get;
            set;
        } = 5;

        //
        // 摘要:
        //     長度占據位元組數,1/2/4個位元組,0表示壓縮編碼整數,默認2
        public int Size
        {
            get;
            set;
        } = 2;


        //
        // 摘要:
        //     過期時間,超過該時間后按廢棄資料處理,默認500ms
        public int Expire
        {
            get;
            set;
        } = 500;


        //
        // 摘要:
        //     編碼,此應用不需要編碼,只需解碼,
        //     按長度將粘包劃分成多個資料包
        //
        // 引數:
        //   context:
        //
        //   msg:
        protected override object Encode(IHandlerContext context, Packet msg)
       { 
           return msg;
       }

        //
        // 摘要:
        //     解碼
        //
        // 引數:
        //   context:
        //
        //   pk:
        protected override IList<Packet> Decode(IHandlerContext context, Packet pk)
        {
            IExtend extend = context.Owner as IExtend;

            LengthCodec packetCodec = extend["Codec"] as LengthCodec;
           

            if (packetCodec == null)
            {
                IExtend extend2 = extend;
                LengthCodec obj = new LengthCodec
                {
                    Expire = Expire,
                    GetLength = ((Packet p) => MessageCodec<Packet>.GetLength(p, Offset, Size))
                };
                packetCodec = obj;
                extend2["Codec"] = obj;
            }
            
            Console.WriteLine("報文解碼前:{0}", BitConverter.ToString(pk.ToArray()));
            IList<Packet> list = packetCodec.Parse(pk);
            Console.WriteLine("報文解碼");
            foreach (var item in list)
            {
                Console.WriteLine("粘包處理結果:{0}", BitConverter.ToString(item.ToArray()));
            }

            return list;
        }

        //
        // 摘要:
        //     連接關閉時,清空粘包編碼器
        //
        // 引數:
        //   context:
        //
        //   reason:
        public override bool Close(IHandlerContext context, string reason)
        {
            IExtend extend = context.Owner as IExtend;
            if (extend != null)
            {
                extend["Codec"] = null;
            }
            return base.Close(context, reason);
        }
    }

6.長度計算委托GetLength

5.3.6中會呼叫如下每個包的長度計算委托,關于委托的使用方法會在下一篇講解,這里不再展開,

//
// 摘要:
//     從資料流中獲取整幀資料長度
//
// 引數:
//   pk:
//
//   offset:
//
//   size:
//
// 回傳結果:
//     資料幀長度(包含頭部長度位)
protected static int GetLength(Packet pk, int offsetint size)
{
    if (offset < 0)
    {
        return pk.Total - pk.Offset;
    }
    int offset2 = pk.Offset;
    if (offset >= pk.Total)
    {
        return 0;
    }
    int num = 0;
    switch (size)
    {
        case 0:
            {
                MemoryStream stream = pk.GetStream();
                if (offset > 0)
                {
                    stream.Seek(offset, SeekOrigiCurrent);
                }
                num = stream.ReadEncodedInt();
                num += (int)(stream.Position - offset);
                break;
            }
        case 1:
            num = pk[offset];
            break;
        case 2:
            num = pk.ReadBytes(offset, 2).ToUInt16();
            break;
        case 4:
            num = (int)pk.ReadBytes(offset, 4).ToUInt32;
            break;
        case -2:
            num = pk.ReadBytes(offset, 2).ToUInt16(0isLittleEndian: false);
            break;
        case -4:
            num = (int)pk.ReadBytes(offset, 4).ToUInt(0, isLittleEndian: false);
            break;
        default:
            throw new NotSupportedException();
    }
    if (num > pk.Total)
    {
        return 0;
    }          
    return num;
}

7.最終粘包拆分效果圖


著作權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 著作權協議,轉載請附上原文出處鏈接和本宣告,

本文鏈接:https://www.cnblogs.com/JerryMouseLi/p/12659903.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/5997.html

標籤:架構設計

上一篇:面試官對于訊息佇列的連環炮

下一篇:b2b2c系統jwt權限原始碼分享part2

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more