0

0

socket传输protobuf字节流的实例详解

零下一度

零下一度

发布时间:2017-06-23 15:56:05

|

4394人浏览过

|

来源于php中文网

原创

版权声明:本文为原创文章,转载请声明 

上一篇主要说的是protobuf字节流的序列化和解析,将protobuf对象序列化为字节流后虽然可以直接传递,但是实际在项目中却不可能真的只是传递protobuf字节流,因为socket的tcp通讯中会出现几个很常见的问题,就是粘包和少包。所谓粘包,简单点说就是socket会将多个较小的包合并到一起发送。因为tcp是面向连接的,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。少包则是指缓存区满后,soket将不完整的包发送到接收端(按我的理解,粘包和少包其实是一个问题)。这样接收端一次接收到的数据就有可能是多个包,为了解决这个问题,在发送数据之前,需要将包的长度也发送出去。于是,包的结构就应该是 消息长度+消息内容。

这一篇,就来说说数据的拼接,干货来了

首先的拼接数据包

 1     ///  2     /// 构建消息数据包 3     ///  4     ///  5     byte[] BuildPackage(IExtensible protobufModel) 6     { 7         if (protobufModel != null) 8         { 9             byte[] b = ProtobufSerilizer.Serialize(protobufModel);10 11             ByteBuffer buf = ByteBuffer.Allocate(b.Length + 4);12             buf.WriteInt(b.Length);13             buf.WriteBytes(b);14             return buf.GetBytes();15         }16         return null;17     }

代码中使用的ByteBuffer工具java中有提供,但是c#中是没有的,源码摘至,不过作者并未在工具中添加获取所有字节码的方法,所以自己添加了一个GetBytes()方法

  1 using System;  2 using System.Collections.Generic;  3   4 ///   5 /// 字节缓冲处理类,本类仅处理大字节序  6 /// 警告,本类非线程安全  7 ///   8 public class ByteBuffer  9 { 10     //字节缓存区 11     private byte[] buf; 12     //读取索引 13     private int readIndex = 0; 14     //写入索引 15     private int writeIndex = 0; 16     //读取索引标记 17     private int markReadIndex = 0; 18     //写入索引标记 19     private int markWirteIndex = 0; 20     //缓存区字节数组的长度 21     private int capacity; 22  23     //对象池 24     private static List pool = new List(); 25     private static int poolMaxCount = 200; 26     //此对象是否池化 27     private bool isPool = false; 28  29     ///  30     /// 构造方法 31     ///  32     /// 初始容量 33     private ByteBuffer(int capacity) 34     { 35         buf = new byte[capacity]; 36         this.capacity = capacity; 37     } 38  39     ///  40     /// 构造方法 41     ///  42     /// 初始字节数组 43     private ByteBuffer(byte[] bytes) 44     { 45         buf = bytes; 46         this.capacity = bytes.Length; 47         this.readIndex = 0; 48         this.writeIndex = bytes.Length + 1; 49     } 50  51     ///  52     /// 构建一个capacity长度的字节缓存区ByteBuffer对象 53     ///  54     /// 初始容量 55     /// ByteBuffer对象 56     public static ByteBuffer Allocate(int capacity) 57     { 58         return new ByteBuffer(capacity); 59     } 60  61     ///  62     /// 构建一个以bytes为字节缓存区的ByteBuffer对象,一般不推荐使用 63     ///  64     /// 初始字节数组 65     /// ByteBuffer对象 66     public static ByteBuffer Allocate(byte[] bytes) 67     { 68         return new ByteBuffer(bytes); 69     } 70  71     ///  72     /// 获取一个池化的ByteBuffer对象,池化的对象必须在调用Dispose后才会推入池中,否则此方法等同于Allocate(int capacity)方法,此方法为线程安全的 73     ///  74     /// ByteBuffer对象的初始容量大小,如果缓存池中没有对象,则对象的容量大小为此值,否则为池中对象的实际容量值 75     ///  76     public static ByteBuffer GetFromPool(int capacity) 77     { 78         lock (pool) 79         { 80             ByteBuffer bbuf; 81             if (pool.Count == 0) 82             { 83                 bbuf = Allocate(capacity); 84                 bbuf.isPool = true; 85                 return bbuf; 86             } 87             int lastIndex = pool.Count - 1; 88             bbuf = pool[lastIndex]; 89             pool.RemoveAt(lastIndex); 90             if (!bbuf.isPool) 91             { 92                 bbuf.isPool = true; 93             } 94             return bbuf; 95         } 96     } 97  98     ///  99     /// 根据length长度,确定大于此leng的最近的2次方数,如length=7,则返回值为8100     /// 101     /// 参考容量102     /// 比参考容量大的最接近的2次方数103     private int FixLength(int length)104     {105         int n = 2;106         int b = 2;107         while (b < length)108         {109             b = 2 << n;110             n++;111         }112         return b;113     }114 115     /// 116     /// 翻转字节数组,如果本地字节序列为低字节序列,则进行翻转以转换为高字节序列117     /// 118     /// 待转为高字节序的字节数组119     /// 高字节序列的字节数组120     private byte[] flip(byte[] bytes)121     {122         if (BitConverter.IsLittleEndian)123         {124124             Array.Reverse(bytes);125         }126         return bytes;127     }128 129     /// 130     /// 确定内部字节缓存数组的大小131     /// 132     /// 当前容量133     /// 将来的容量134     /// 将来的容量135     private int FixSizeAndReset(int currLen, int futureLen)136     {137         if (futureLen > currLen)138         {139             //以原大小的2次方数的两倍确定内部字节缓存区大小140             int size = FixLength(currLen) * 2;141             if (futureLen > size)142             {143                 //以将来的大小的2次方的两倍确定内部字节缓存区大小144                 size = FixLength(futureLen) * 2;145             }146             byte[] newbuf = new byte[size];147             Array.Copy(buf, 0, newbuf, 0, currLen);148             buf = newbuf;149             capacity = newbuf.Length;150         }151         return futureLen;152     }153 154     /// 155     /// 将bytes字节数组从startIndex开始的length字节写入到此缓存区156     /// 157     /// 待写入的字节数据158     /// 写入的开始位置159     /// 写入的长度160     public void WriteBytes(byte[] bytes, int startIndex, int length)161     {162         int offset = length - startIndex;163         if (offset <= 0) return;164         int total = offset + writeIndex;165         int len = buf.Length;166         FixSizeAndReset(len, total);167         for (int i = writeIndex, j = startIndex; i < total; i++, j++)168         {169             buf[i] = bytes[j];170         }171         writeIndex = total;172     }173 174     /// 175     /// 将字节数组中从0到length的元素写入缓存区176     /// 177     /// 待写入的字节数据178     /// 写入的长度179     public void WriteBytes(byte[] bytes, int length)180     {181         WriteBytes(bytes, 0, length);182     }183 184     /// 185     /// 将字节数组全部写入缓存区186     /// 187     /// 待写入的字节数据188     public void WriteBytes(byte[] bytes)189     {190         WriteBytes(bytes, bytes.Length);191     }192 193     /// 194     /// 将一个ByteBuffer的有效字节区写入此缓存区中195     /// 196     /// 待写入的字节缓存区197     public void Write(ByteBuffer buffer)198     {199         if (buffer == null) return;200         if (buffer.ReadableBytes() <= 0) return;201         WriteBytes(buffer.ToArray());202     }203 204     /// 205     /// 写入一个int16数据206     /// 207     /// short数据208     public void WriteShort(short value)209     {210         WriteBytes(flip(BitConverter.GetBytes(value)));211     }212 213     /// 214     /// 写入一个ushort数据215     /// 216     /// ushort数据217     public void WriteUshort(ushort value)218     {219         WriteBytes(flip(BitConverter.GetBytes(value)));220     }221 222     /// 223     /// 写入一个int32数据224     /// 225     /// int数据226     public void WriteInt(int value)227     {228         //byte[] array = new byte[4];229         //for (int i = 3; i >= 0; i--)230         //{231         //    array[i] = (byte)(value & 0xff);232         //    value = value >> 8;233         //}234         //Array.Reverse(array);235         //Write(array);236         WriteBytes(flip(BitConverter.GetBytes(value)));237     }238 239     /// 240     /// 写入一个uint32数据241     /// 242     /// uint数据243     public void WriteUint(uint value)244     {245         WriteBytes(flip(BitConverter.GetBytes(value)));246     }247 248     /// 249     /// 写入一个int64数据250     /// 251     /// long数据252     public void WriteLong(long value)253     {254         WriteBytes(flip(BitConverter.GetBytes(value)));255     }256 257     /// 258     /// 写入一个uint64数据259     /// 260     /// ulong数据261     public void WriteUlong(ulong value)262     {263         WriteBytes(flip(BitConverter.GetBytes(value)));264     }265 266     /// 267     /// 写入一个float数据268     /// 269     /// float数据270     public void WriteFloat(float value)271     {272         WriteBytes(flip(BitConverter.GetBytes(value)));273     }274 275     /// 276     /// 写入一个byte数据277     /// 278     /// byte数据279     public void WriteByte(byte value)280     {281         int afterLen = writeIndex + 1;282         int len = buf.Length;283         FixSizeAndReset(len, afterLen);284         buf[writeIndex] = value;285         writeIndex = afterLen;286     }287 288     /// 289     /// 写入一个byte数据290     /// 291     /// byte数据292     public void WriteByte(int value)293     {294         byte b = (byte)value;295         WriteByte(b);296     }297 298     /// 299     /// 写入一个double类型数据300     /// 301     /// double数据302     public void WriteDouble(double value)303     {304         WriteBytes(flip(BitConverter.GetBytes(value)));305     }306 307     /// 308     /// 写入一个字符309     /// 310     /// 311     public void WriteChar(char value)312     {313         WriteBytes(flip(BitConverter.GetBytes(value)));314     }315 316     /// 317     /// 写入一个布尔型数据318     /// 319     /// 320     public void WriteBoolean(bool value)321     {322         WriteBytes(flip(BitConverter.GetBytes(value)));323     }324 325     /// 326     /// 读取一个字节327     /// 328     /// 字节数据329     public byte ReadByte()330     {331         byte b = buf[readIndex];332         readIndex++;333         return b;334     }335 336     /// 337     /// 读取一个字节并转为int类型的数据338     /// 339     /// int数据340     public int ReadByteToInt()341     {342         byte b = ReadByte();343         return (int)b;344     }345 346     /// 347     /// 获取从index索引处开始len长度的字节348     /// 349     /// 350     /// 351     /// 352     private byte[] Get(int index, int len)353     {354         byte[] bytes = new byte[len];355         Array.Copy(buf, index, bytes, 0, len);356         return flip(bytes);357     }358 359     /// 360     /// 从读取索引位置开始读取len长度的字节数组361     /// 362     /// 待读取的字节长度363     /// 字节数组364     private byte[] Read(int len)365     {366         byte[] bytes = Get(readIndex, len);367         readIndex += len;368         return bytes;369     }370 371     /// 372     /// 读取一个uint16数据373     /// 374     /// ushort数据375     public ushort ReadUshort()376     {377         return BitConverter.ToUInt16(Read(2), 0);378     }379 380     /// 381     /// 读取一个int16数据382     /// 383     /// short数据384     public short ReadShort()385     {386         return BitConverter.ToInt16(Read(2), 0);387     }388 389     /// 390     /// 读取一个uint32数据391     /// 392     /// uint数据393     public uint ReadUint()394     {395         return BitConverter.ToUInt32(Read(4), 0);396     }397 398     /// 399     /// 读取一个int32数据400     /// 401     /// int数据402     public int ReadInt()403     {404         return BitConverter.ToInt32(Read(4), 0);405     }406 407     /// 408     /// 读取一个uint64数据409     /// 410     /// ulong数据411     public ulong ReadUlong()412     {413         return BitConverter.ToUInt64(Read(8), 0);414     }415 416     /// 417     /// 读取一个long数据418     /// 419     /// long数据420     public long ReadLong()421     {422         return BitConverter.ToInt64(Read(8), 0);423     }424 425     /// 426     /// 读取一个float数据427     /// 428     /// float数据429     public float ReadFloat()430     {431         return BitConverter.ToSingle(Read(4), 0);432     }433 434     /// 435     /// 读取一个double数据436     /// 437     /// double数据438     public double ReadDouble()439     {440         return BitConverter.ToDouble(Read(8), 0);441     }442 443     /// 444     /// 读取一个字符445     /// 446     /// 447     public char ReadChar()448     {449         return BitConverter.ToChar(Read(2), 0);450     }451 452     /// 453     /// 读取布尔型数据454     /// 455     /// 456     public bool ReadBoolean()457     {458         return BitConverter.ToBoolean(Read(1), 0);459     }460 461     /// 462     /// 从读取索引位置开始读取len长度的字节到disbytes目标字节数组中463     /// 464     /// 读取的字节将存入此字节数组465     /// 目标字节数组的写入索引466     /// 读取的长度467     public void ReadBytes(byte[] disbytes, int disstart, int len)468     {469         int size = disstart + len;470         for (int i = disstart; i < size; i++)471         {472             disbytes[i] = this.ReadByte();473         }474     }475 476     /// 477     /// 获取一个字节478     /// 479     /// 480     /// 481     public byte GetByte(int index)482     {483         return buf[index];484     }485 486     /// 487     /// 获取全部字节488     /// 489     /// 490     public byte[] GetBytes()491     {492         return buf;493     }494 495     /// 496     /// 获取一个双精度浮点数据,不改变数据内容497     /// 498     /// 字节索引499     /// 500     public double GetDouble(int index)501     {502         return BitConverter.ToDouble(Get(0, 8), 0);503     }504 505     /// 506     /// 获取一个浮点数据,不改变数据内容507     /// 508     /// 字节索引509     /// 510     public float GetFloat(int index)511     {512         return BitConverter.ToSingle(Get(0, 4), 0);513     }514 515     /// 516     /// 获取一个长整形数据,不改变数据内容517     /// 518     /// 字节索引519     /// 520     public long GetLong(int index)521     {522         return BitConverter.ToInt64(Get(0, 8), 0);523     }524 525     /// 526     /// 获取一个整形数据,不改变数据内容527     /// 528     /// 字节索引529     /// 530     public int GetInt(int index)531     {532         return BitConverter.ToInt32(Get(0, 4), 0);533     }534 535     /// 536     /// 获取一个短整形数据,不改变数据内容537     /// 538     /// 字节索引539     /// 540     public int GetShort(int index)541     {542         return BitConverter.ToInt16(Get(0, 2), 0);543     }544 545 546     /// 547     /// 清除已读字节并重建缓存区548     /// 549     public void DiscardReadBytes()550     {551         if (readIndex <= 0) return;552         int len = buf.Length - readIndex;553         byte[] newbuf = new byte[len];554         Array.Copy(buf, readIndex, newbuf, 0, len);555         buf = newbuf;556         writeIndex -= readIndex;557         markReadIndex -= readIndex;558         if (markReadIndex < 0)559         {560             markReadIndex = readIndex;561         }562         markWirteIndex -= readIndex;563         if (markWirteIndex < 0 || markWirteIndex < readIndex || markWirteIndex < markReadIndex)564         {565             markWirteIndex = writeIndex;566         }567         readIndex = 0;568     }569 570     /// 571     /// 清空此对象,但保留字节缓存数组(空数组)572     /// 573     public void Clear()574     {575         buf = new byte[buf.Length];576         readIndex = 0;577         writeIndex = 0;578         markReadIndex = 0;579         markWirteIndex = 0;580         capacity = buf.Length;581     }582     583     /// 584     /// 释放对象,清除字节缓存数组,如果此对象为可池化,那么调用此方法将会把此对象推入到池中等待下次调用585     /// 586     public void Dispose()587     {588         readIndex = 0;589         writeIndex = 0;590         markReadIndex = 0;591         markWirteIndex = 0;592         if (isPool)593         {594             lock (pool)595             {596                 if (pool.Count < poolMaxCount)597                 {598                     pool.Add(this);599                 }600             }601         }602         else603         {604             capacity = 0;605             buf = null;606         }607     }608 609     /// 610     /// 设置/获取读指针位置611     /// 612     public int ReaderIndex613     {614         get615         {616             return readIndex;617         }618         set619         {620             if (value < 0) return;621             readIndex = value;622         }623     }624 625     /// 626     /// 设置/获取写指针位置627     /// 628     public int WriterIndex629     {630         get631         {632             return writeIndex;633         }634         set635         {636             if (value < 0) return;637             writeIndex = value;638         }639     }640 641     /// 642     /// 标记读取的索引位置643     /// 644     public void MarkReaderIndex()645     {646         markReadIndex = readIndex;647     }648 649     /// 650     /// 标记写入的索引位置651     /// 652     public void MarkWriterIndex()653     {654         markWirteIndex = writeIndex;655     }656 657     /// 658     /// 将读取的索引位置重置为标记的读取索引位置659     /// 660     public void ResetReaderIndex()661     {662         readIndex = markReadIndex;663     }664 665     /// 666     /// 将写入的索引位置重置为标记的写入索引位置667     /// 668     public void ResetWriterIndex()669     {670         writeIndex = markWirteIndex;671     }672 673     /// 674     /// 可读的有效字节数675     /// 676     /// 可读的字节数677     public int ReadableBytes()678     {679         return writeIndex - readIndex;680     }681 682     /// 683     /// 获取可读的字节数组684     /// 685     /// 字节数据686     public byte[] ToArray()687     {688         byte[] bytes = new byte[writeIndex];689         Array.Copy(buf, 0, bytes, 0, bytes.Length);690         return bytes;691     }692 693     /// 694     /// 获取缓存区容量大小695     /// 696     /// 缓存区容量697     public int GetCapacity()698     {699         return this.capacity;700     }701 702     /// 703     /// 简单的数据类型704     /// 705     public enum LengthType706     {707         //byte类型708         BYTE,709         //short类型710         SHORT,711         //int类型712         INT713     }714 715     /// 716     /// 写入一个数据717     /// 718     /// 待写入的数据719     /// 待写入的数据类型720     public void WriteValue(int value, LengthType type)721     {722         switch (type)723         {724             case LengthType.BYTE:725                 this.WriteByte(value);726                 break;727             case LengthType.SHORT:728                 this.WriteShort((short)value);729                 break;730             default:731                 this.WriteInt(value);732                 break;733         }734     }735 736     /// 737     /// 读取一个值,值类型根据type决定,int或short或byte738     /// 739     /// 值类型740     /// int数据741     public int ReadValue(LengthType type)742     {743         switch (type)744         {745             case LengthType.BYTE:746                 return ReadByteToInt();747             case LengthType.SHORT:748                 return (int)ReadShort();749             default:750                 return ReadInt();751         }752     }753 754     /// 755     /// 写入一个字符串756     /// 757     /// 待写入的字符串758     /// 写入的字符串长度类型759     public void WriteUTF8String(string content, LengthType lenType)760     {761         byte[] bytes = System.Text.UTF8Encoding.UTF8.GetBytes(content);762         int max;763         if (lenType == LengthType.BYTE)764         {765             WriteByte(bytes.Length);766             max = byte.MaxValue;767         }768         else if (lenType == LengthType.SHORT)769         {770             WriteShort((short)bytes.Length);771             max = short.MaxValue;772         }773         else774         {775             WriteInt(bytes.Length);776             max = int.MaxValue;777         }778         if (bytes.Length > max)779         {780             WriteBytes(bytes, 0, max);781         }782         else783         {784             WriteBytes(bytes, 0, bytes.Length);785         }786     }787 788     /// 789     /// 读取一个字符串790     /// 791     /// 需读取的字符串长度792     /// 字符串793     public string ReadUTF8String(int len)794     {795         byte[] bytes = new byte[len];796         this.ReadBytes(bytes, 0, len);797         return System.Text.UTF8Encoding.UTF8.GetString(bytes);798     }799 800     /// 801     /// 读取一个字符串802     /// 803     /// 字符串长度类型804     /// 字符串805     public string ReadUTF8String(LengthType lenType)806     {807         int len = ReadValue(lenType);808         return ReadUTF8String(len);809     }810 811     /// 812     /// 复制一个对象,具有与原对象相同的数据,不改变原对象的数据813     /// 814     /// 815     public ByteBuffer Copy()816     {817         return Copy(0);818     }819 820     public ByteBuffer Copy(int startIndex)821     {822         if (buf == null)823         {824             return new ByteBuffer(16);825         }826         byte[] target = new byte[buf.Length - startIndex];827         Array.Copy(buf, startIndex, target, 0, target.Length);828         ByteBuffer buffer = new ByteBuffer(target.Length);829         buffer.WriteBytes(target);830         return buffer;831     }832 }
View Code

当然,c#中虽然没有ByteBuffer,但也有拼接字节数组的方法,比如

      Send(          [] bytes =  [data.Length +          [] length = BitConverter.GetBytes(                                     Array.Copy(length, , bytes, ,          Array.Copy(data, , bytes,          mSocket.Send(bytes);
     }

字节数组拼接好后,就可以使用socket的send方法发送了,不过这一篇先继续讲完接收数据的处理

AVCLabs
AVCLabs

AI移除视频背景,100%自动和免费

下载

接收数据的顺序是先接收消息长度,然后根据消息长度接收指定长度的消息

 1     void ReceiveMessage() 2     { 3           //上文说过,一个完整的消息是 消息长度+消息内容 4           //所以先创建一个长度4的字节数组,用于接收消息长度 5           byte[] recvBytesHead = GetBytesReceive(4); 6           //将消息长度字节组转为int数值 7           int bodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(recvBytesHead, 0)); 8           //根据消息长度接收指定长度的字节组,这个字节组就是完整的消息内容 9           byte[] recvBytesBody = GetBytesReceive(bodyLength);10           //最后反序列化消息的内容11           Test message = ProtobufSerilizer.DeSerialize(messageBody);12     }

GetBytesRecive方法用于接收消息,并解决粘包、少包的问题,代码如下

 1     ///  2     /// 接收数据并处理 3     ///  4     ///  5     ///  6     byte[] GetBytesReceive(int length) 7     { 8         //创建指定长度的字节组 9         byte[] recvBytes = new byte[length];10         //设置每次接收包的最大长度为1024个字节11         int packageMaxLength = 1024;12         //使用循环来保证接收的数据是完整的,如果剩余长度大于0,证明接收未完成13         while (length > 0)14         {15             //创建字节组,用于存放需要接收的字节流16             byte[] receiveBytes = new byte[length < packageMaxLength ? length : packageMaxLength];17             int iBytesBody = 0;18             //根据剩余需接收的长度来设置接收数据的长度19             if (length >= receiveBytes.Length)20                 iBytesBody = mSocket.Receive(receiveBytes, receiveBytes.Length, 0);21             else22                 iBytesBody = mSocket.Receive(receiveBytes, length, 0);23             receiveBytes.CopyTo(recvBytes, recvBytes.Length - length);24             //减去已接收的长度25             length -= iBytesBody;26         }27         return recvBytes;28     }

到这里,消息的简单发送和接收就基本搞定了,但是,实际项目中,我们的消息数量肯定不会只有一条,如果是长链接的项目,更是需要一直接收和发送消息,该怎么办?

众所周知,unity的UI上的显示只能在主线程中执行,可是如果我们在主线程一直接收和发送消息,那体验将会极差,所以我们必须另外开启线程来负责消息的接收和发送,下一篇就是使用多线程来完成socket通讯

 

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

10

2026.01.27

拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

109

2026.01.26

edge浏览器怎样设置主页 edge浏览器自定义设置教程
edge浏览器怎样设置主页 edge浏览器自定义设置教程

在Edge浏览器中设置主页,请依次点击右上角“...”图标 > 设置 > 开始、主页和新建标签页。在“Microsoft Edge 启动时”选择“打开以下页面”,点击“添加新页面”并输入网址。若要使用主页按钮,需在“外观”设置中开启“显示主页按钮”并设定网址。

16

2026.01.26

苹果官方查询网站 苹果手机正品激活查询入口
苹果官方查询网站 苹果手机正品激活查询入口

苹果官方查询网站主要通过 checkcoverage.apple.com/cn/zh/ 进行,可用于查询序列号(SN)对应的保修状态、激活日期及技术支持服务。此外,查找丢失设备请使用 iCloud.com/find,购买信息与物流可访问 Apple (中国大陆) 订单状态页面。

136

2026.01.26

npd人格什么意思 npd人格有什么特征
npd人格什么意思 npd人格有什么特征

NPD(Narcissistic Personality Disorder)即自恋型人格障碍,是一种心理健康问题,特点是极度夸大自我重要性、需要过度赞美与关注,同时极度缺乏共情能力,背后常掩藏着低自尊和不安全感,影响人际关系、工作和生活,通常在青少年时期开始显现,需由专业人士诊断。

7

2026.01.26

windows安全中心怎么关闭 windows安全中心怎么执行操作
windows安全中心怎么关闭 windows安全中心怎么执行操作

关闭Windows安全中心(Windows Defender)可通过系统设置暂时关闭,或使用组策略/注册表永久关闭。最简单的方法是:进入设置 > 隐私和安全性 > Windows安全中心 > 病毒和威胁防护 > 管理设置,将实时保护等选项关闭。

6

2026.01.26

2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】
2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】

铁路12306提供起售时间查询、起售提醒、购票预填、候补购票及误购限时免费退票五项服务,并强调官方渠道唯一性与信息安全。

122

2026.01.26

个人所得税税率表2026 个人所得税率最新税率表
个人所得税税率表2026 个人所得税率最新税率表

以工资薪金所得为例,应纳税额 = 应纳税所得额 × 税率 - 速算扣除数。应纳税所得额 = 月度收入 - 5000 元 - 专项扣除 - 专项附加扣除 - 依法确定的其他扣除。假设某员工月工资 10000 元,专项扣除 1000 元,专项附加扣除 2000 元,当月应纳税所得额为 10000 - 5000 - 1000 - 2000 = 2000 元,对应税率为 3%,速算扣除数为 0,则当月应纳税额为 2000×3% = 60 元。

35

2026.01.26

oppo云服务官网登录入口 oppo云服务登录手机版
oppo云服务官网登录入口 oppo云服务登录手机版

oppo云服务https://cloud.oppo.com/可以在云端安全存储您的照片、视频、联系人、便签等重要数据。当您的手机数据意外丢失或者需要更换手机时,可以随时将这些存储在云端的数据快速恢复到手机中。

121

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
进程与SOCKET
进程与SOCKET

共6课时 | 0.4万人学习

golang socket 编程
golang socket 编程

共2课时 | 0.1万人学习

swoole入门物联网开发与实战
swoole入门物联网开发与实战

共15课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号