kcp-go源码解析

概念

ARQ:自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层的错误纠正协议之一.
RTO:Retransmission TimeOut
FEC:Forward Error Correction

kcp简介

kcp是一个基于udp实现快速、可靠、向前纠错的的协议,能以比TCP浪费10%-20%的带宽的代价,换取平均延迟降低30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发。查看官方文档kcp

kcp-go是用go实现了kcp协议的一个库,其实kcp类似tcp,协议的实现也很多参考tcp协议的实现,滑动窗口,快速重传,选择性重传,慢启动等。
kcp和tcp一样,也分客户端和监听端。

    +-+-+-+-+-+            +-+-+-+-+-+|  Client |            |  Server |+-+-+-+-+-+            +-+-+-+-+-+|------ kcp data ------>|     |<----- kcp data -------|     

kcp协议

layer model

+----------------------+
|      Session         |
+----------------------+
|      KCP(ARQ)        |
+----------------------+
|      FEC(OPTIONAL)   |
+----------------------+
|      CRYPTO(OPTIONAL)|
+----------------------+
|      UDP(Packet)     |
+----------------------+

KCP header

KCP Header Format

      4           1   1     2 (Byte)
+---+---+---+---+---+---+---+---+
|     conv      |cmd|frg|  wnd  |
+---+---+---+---+---+---+---+---+
|     ts        |     sn        |
+---+---+---+---+---+---+---+---+
|     una       |     len       |
+---+---+---+---+---+---+---+---+
|                               |
+             DATA              +
|                               |
+---+---+---+---+---+---+---+---+

代码结构

src/vendor/github.com/xtaci/kcp-go/
├── LICENSE
├── README.md
├── crypt.go    加解密实现
├── crypt_test.go
├── donate.png
├── fec.go      向前纠错实现
├── frame.png
├── kcp-go.png
├── kcp.go      kcp协议实现
├── kcp_test.go
├── sess.go     会话管理实现
├── sess_test.go
├── snmp.go     数据统计实现
├── updater.go  任务调度实现
├── xor.go      xor封装
└── xor_test.go

着重研究两个文件kcp.gosess.go

kcp浅析

kcp是基于udp实现的,所有udp的实现这里不做介绍,kcp做的事情就是怎么封装udp的数据和怎么解析udp的数据,再加各种处理机制,为了重传,拥塞控制,纠错等。下面介绍kcp客户端和服务端整体实现的流程,只是大概介绍一下函数流,不做详细解析,详细解析看后面数据流的解析。

kcp client整体函数流

和tcp一样,kcp要连接服务端需要先拨号,但是和tcp有个很大的不同是,即使服务端没有启动,客户端一样可以拨号成功,因为实际上这里的拨号没有发送任何信息,而tcp在这里需要三次握手。

DialWithOptions(raddr string, block BlockCrypt, dataShards, parityShards int)V
net.DialUDP("udp", nil, udpaddr)V
NewConn()V
newUDPSession() {初始化UDPSession}V
NewKCP() {初始化kcp}V
updater.addSession(sess) {管理session会话,任务管理,根据用户设置的internal参数间隔来轮流唤醒任务}V
go sess.readLoop()V
go s.receiver(chPacket)V
s.kcpInput(data)V
s.fecDecoder.decodeBytes(data)V
s.kcp.Input(data, true, s.ackNoDelay)V
kcp.parse_data(seg) {将分段好的数据插入kcp.rcv_buf缓冲}V
notifyReadEvent()

客户端大体的流程如上面所示,先Dial,建立udp连接,将这个连接封装成一个会话,然后启动一个go程,接收udp的消息。

kcp server整体函数流

ListenWithOptions() V
net.ListenUDP()V
ServerConn()V
newFECDecoder()V
go l.monitor() {从chPacket接收udp数据,写入kcp}V
go l.receiver(chPacket) {从upd接收数据,并入队列}V
newUDPSession()V
updater.addSession(sess) {管理session会话,任务管理,根据用户设置的internal参数间隔来轮流唤醒任务}V
s.kcpInput(data)`V
s.fecDecoder.decodeBytes(data)V
s.kcp.Input(data, true, s.ackNoDelay)V
kcp.parse_data(seg) {将分段好的数据插入kcp.rcv_buf缓冲}V
notifyReadEvent()

服务端的大体流程如上图所示,先Listen,启动udp监听,接着用一个go程监控udp的数据包,负责将不同session的数据写入不同的udp连接,然后解析封装将数据交给上层。

kcp 数据流详细解析

不管是kcp的客户端还是服务端,他们都有io行为,就是读与写,我们只分析一个就好了,因为它们读写的实现是一样的,这里分析客户端的读与写。

kcp client 发送消息

s.Write(b []byte) V
s.kcp.WaitSnd() {}V
s.kcp.Send(b) {将数据根据mss分段,并存在kcp.snd_queue}V
s.kcp.flush(false) [flush data to output] {if writeDelay==true {flush}else{每隔`interval`时间flush一次}
}V
kcp.output(buffer, size) V
s.output(buf)V
s.conn.WriteTo(ext, s.remote)V
s.conn..Conn.WriteTo(buf)

读写都是在sess.go文件中实现的,Write方法:

// Write implements net.Conn
func (s *UDPSession) Write(b []byte) (n int, err error) {for {...// api flow controlif s.kcp.WaitSnd() < int(s.kcp.snd_wnd) {n = len(b)for {if len(b) <= int(s.kcp.mss) {s.kcp.Send(b)break} else {s.kcp.Send(b[:s.kcp.mss])b = b[s.kcp.mss:]}}if !s.writeDelay {s.kcp.flush(false)}s.mu.Unlock()atomic.AddUint64(&DefaultSnmp.BytesSent, uint64(n))return n, nil}...// wait for write event or timeoutselect {case <-s.chWriteEvent:case <-c:case <-s.die:}if timeout != nil {timeout.Stop()}}
}

假设发送一个hello消息,Write方法会先判断发送窗口是否已满,满的话该函数阻塞,不满则kcp.Send(“hello”),而Send函数实现根据mss的值对数据分段,当然这里的发送的hello,长度太短,只分了一个段,并把它们插入发送的队列里。

func (kcp *KCP) Send(buffer []byte) int {...for i := 0; i < count; i++ {var size intif len(buffer) > int(kcp.mss) {size = int(kcp.mss)} else {size = len(buffer)}seg := kcp.newSegment(size)copy(seg.data, buffer[:size])if kcp.stream == 0 { // message modeseg.frg = uint8(count - i - 1)} else { // stream modeseg.frg = 0}kcp.snd_queue = append(kcp.snd_queue, seg)buffer = buffer[size:]}return 0
}

接着判断参数writeDelay,如果参数设置为false,则立马发送消息,否则需要任务调度后才会触发发送,发送消息是由flush函数实现的。

// flush pending data
func (kcp *KCP) flush(ackOnly bool) {var seg Segmentseg.conv = kcp.convseg.cmd = IKCP_CMD_ACKseg.wnd = kcp.wnd_unused()seg.una = kcp.rcv_nxtbuffer := kcp.buffer// flush acknowledgesptr := bufferfor i, ack := range kcp.acklist {size := len(buffer) - len(ptr)if size+IKCP_OVERHEAD > int(kcp.mtu) {kcp.output(buffer, size)ptr = buffer}// filter jitters caused by bufferbloatif ack.sn >= kcp.rcv_nxt || len(kcp.acklist)-1 == i {seg.sn, seg.ts = ack.sn, ack.tsptr = seg.encode(ptr)}}kcp.acklist = kcp.acklist[0:0]if ackOnly { // flash remain ack segmentssize := len(buffer) - len(ptr)if size > 0 {kcp.output(buffer, size)}return}// probe window size (if remote window size equals zero)if kcp.rmt_wnd == 0 {current := currentMs()if kcp.probe_wait == 0 {kcp.probe_wait = IKCP_PROBE_INITkcp.ts_probe = current + kcp.probe_wait} else {if _itimediff(current, kcp.ts_probe) >= 0 {if kcp.probe_wait < IKCP_PROBE_INIT {kcp.probe_wait = IKCP_PROBE_INIT}kcp.probe_wait += kcp.probe_wait / 2if kcp.probe_wait > IKCP_PROBE_LIMIT {kcp.probe_wait = IKCP_PROBE_LIMIT}kcp.ts_probe = current + kcp.probe_waitkcp.probe |= IKCP_ASK_SEND}}} else {kcp.ts_probe = 0kcp.probe_wait = 0}// flush window probing commandsif (kcp.probe & IKCP_ASK_SEND) != 0 {seg.cmd = IKCP_CMD_WASKsize := len(buffer) - len(ptr)if size+IKCP_OVERHEAD > int(kcp.mtu) {kcp.output(buffer, size)ptr = buffer}ptr = seg.encode(ptr)}// flush window probing commandsif (kcp.probe & IKCP_ASK_TELL) != 0 {seg.cmd = IKCP_CMD_WINSsize := len(buffer) - len(ptr)if size+IKCP_OVERHEAD > int(kcp.mtu) {kcp.output(buffer, size)ptr = buffer}ptr = seg.encode(ptr)}kcp.probe = 0// calculate window sizecwnd := _imin_(kcp.snd_wnd, kcp.rmt_wnd)if kcp.nocwnd == 0 {cwnd = _imin_(kcp.cwnd, cwnd)}// sliding window, controlled by snd_nxt && sna_una+cwndnewSegsCount := 0for k := range kcp.snd_queue {if _itimediff(kcp.snd_nxt, kcp.snd_una+cwnd) >= 0 {break}newseg := kcp.snd_queue[k]newseg.conv = kcp.convnewseg.cmd = IKCP_CMD_PUSHnewseg.sn = kcp.snd_nxtkcp.snd_buf = append(kcp.snd_buf, newseg)kcp.snd_nxt++newSegsCount++kcp.snd_queue[k].data = nil}if newSegsCount > 0 {kcp.snd_queue = kcp.remove_front(kcp.snd_queue, newSegsCount)}// calculate resentresent := uint32(kcp.fastresend)if kcp.fastresend <= 0 {resent = 0xffffffff}// check for retransmissionscurrent := currentMs()var change, lost, lostSegs, fastRetransSegs, earlyRetransSegs uint64for k := range kcp.snd_buf {segment := &kcp.snd_buf[k]needsend := falseif segment.xmit == 0 { // initial transmitneedsend = truesegment.rto = kcp.rx_rtosegment.resendts = current + segment.rto} else if _itimediff(current, segment.resendts) >= 0 { // RTOneedsend = trueif kcp.nodelay == 0 {segment.rto += kcp.rx_rto} else {segment.rto += kcp.rx_rto / 2}segment.resendts = current + segment.rtolost++lostSegs++} else if segment.fastack >= resent { // fast retransmitneedsend = truesegment.fastack = 0segment.rto = kcp.rx_rtosegment.resendts = current + segment.rtochange++fastRetransSegs++} else if segment.fastack > 0 && newSegsCount == 0 { // early retransmitneedsend = truesegment.fastack = 0segment.rto = kcp.rx_rtosegment.resendts = current + segment.rtochange++earlyRetransSegs++}if needsend {segment.xmit++segment.ts = currentsegment.wnd = seg.wndsegment.una = seg.unasize := len(buffer) - len(ptr)need := IKCP_OVERHEAD + len(segment.data)if size+need > int(kcp.mtu) {kcp.output(buffer, size)current = currentMs() // time update for a blocking callptr = buffer}ptr = segment.encode(ptr)copy(ptr, segment.data)ptr = ptr[len(segment.data):]if segment.xmit >= kcp.dead_link {kcp.state = 0xFFFFFFFF}}}// flash remain segmentssize := len(buffer) - len(ptr)if size > 0 {kcp.output(buffer, size)}// counter updatessum := lostSegsif lostSegs > 0 {atomic.AddUint64(&DefaultSnmp.LostSegs, lostSegs)}if fastRetransSegs > 0 {atomic.AddUint64(&DefaultSnmp.FastRetransSegs, fastRetransSegs)sum += fastRetransSegs}if earlyRetransSegs > 0 {atomic.AddUint64(&DefaultSnmp.EarlyRetransSegs, earlyRetransSegs)sum += earlyRetransSegs}if sum > 0 {atomic.AddUint64(&DefaultSnmp.RetransSegs, sum)}// update ssthresh// rate halving, https://tools.ietf.org/html/rfc6937if change > 0 {inflight := kcp.snd_nxt - kcp.snd_unakcp.ssthresh = inflight / 2if kcp.ssthresh < IKCP_THRESH_MIN {kcp.ssthresh = IKCP_THRESH_MIN}kcp.cwnd = kcp.ssthresh + resentkcp.incr = kcp.cwnd * kcp.mss}// congestion control, https://tools.ietf.org/html/rfc5681if lost > 0 {kcp.ssthresh = cwnd / 2if kcp.ssthresh < IKCP_THRESH_MIN {kcp.ssthresh = IKCP_THRESH_MIN}kcp.cwnd = 1kcp.incr = kcp.mss}if kcp.cwnd < 1 {kcp.cwnd = 1kcp.incr = kcp.mss}
}

flush函数非常的重要,kcp的重要参数都是在调节这个函数的行为,这个函数只有一个参数ackOnly,意思就是只发送ack,如果ackOnly为true的话,该函数只遍历ack列表,然后发送,就完事了。 如果不是,也会发送真实数据。 在发送数据前先进行windSize探测,如果开启了拥塞控制nc=0,则每次发送前检测服务端的winsize,如果服务端的winsize变小了,自身的winsize也要更着变小,来避免拥塞。如果没有开启拥塞控制,就按设置的winsize进行数据发送。
接着循环每个段数据,并判断每个段数据的是否该重发,还有什么时候重发:
1. 如果这个段数据首次发送,则直接发送数据。 2. 如果这个段数据的当前时间大于它自身重发的时间,也就是RTO,则重传消息。 3. 如果这个段数据的ack丢失累计超过resent次数,则重传,也就是快速重传机制。这个resent参数由resend参数决定。 4. 如果这个段数据的ack有丢失且没有新的数据段,则触发ER,ER相关信息ER

最后通过kcp.output发送消息hello,output是个回调函数,函数的实体是sess.go的:

func (s *UDPSession) output(buf []byte) {var ecc [][]byte// extend buf's header spaceext := bufif s.headerSize > 0 {ext = s.ext[:s.headerSize+len(buf)]copy(ext[s.headerSize:], buf)}// FEC stageif s.fecEncoder != nil {ecc = s.fecEncoder.Encode(ext)}// encryption stageif s.block != nil {io.ReadFull(rand.Reader, ext[:nonceSize])checksum := crc32.ChecksumIEEE(ext[cryptHeaderSize:])binary.LittleEndian.PutUint32(ext[nonceSize:], checksum)s.block.Encrypt(ext, ext)if ecc != nil {for k := range ecc {io.ReadFull(rand.Reader, ecc[k][:nonceSize])checksum := crc32.ChecksumIEEE(ecc[k][cryptHeaderSize:])binary.LittleEndian.PutUint32(ecc[k][nonceSize:], checksum)s.block.Encrypt(ecc[k], ecc[k])}}}// WriteTo kernelnbytes := 0npkts := 0// if mrand.Intn(100) < 50 {for i := 0; i < s.dup+1; i++ {if n, err := s.conn.WriteTo(ext, s.remote); err == nil {nbytes += nnpkts++}}// }if ecc != nil {for k := range ecc {if n, err := s.conn.WriteTo(ecc[k], s.remote); err == nil {nbytes += nnpkts++}}}atomic.AddUint64(&DefaultSnmp.OutPkts, uint64(npkts))atomic.AddUint64(&DefaultSnmp.OutBytes, uint64(nbytes))
}

output函数才是真正的将数据写入内核中,在写入之前先进行了fec编码,fec编码器的实现是用了一个开源库github.com/klauspost/reedsolomon,编码以后的hello就不是和原来的hello一样了,至少多了几个字节。 fec编码器有两个重要的参数reedsolomon.New(dataShards, parityShards, reedsolomon.WithMaxGoroutines(1)),dataShardsparityShards,这两个参数决定了fec的冗余度,冗余度越大抗丢包性就越强。

kcp的任务调度器

其实这里任务调度器是一个很简单的实现,用一个全局变量updater来管理session,代码文件为updater.go。其中最主要的函数

func (h *updateHeap) updateTask() {var timer <-chan time.Timefor {select {case <-timer:case <-h.chWakeUp:}h.mu.Lock()hlen := h.Len()now := time.Now()if hlen > 0 && now.After(h.entries[0].ts) {for i := 0; i < hlen; i++ {entry := heap.Pop(h).(entry)if now.After(entry.ts) {entry.ts = now.Add(entry.s.update())heap.Push(h, entry)} else {heap.Push(h, entry)break}}}if hlen > 0 {timer = time.After(h.entries[0].ts.Sub(now))}h.mu.Unlock()}
}

任务调度器实现了一个堆结构,每当有新的连接,session都会插入到这个堆里,接着for循环每隔interval时间,遍历这个堆,得到entry然后执行entry.s.update()。而entry.s.update()会执行s.kcp.flush(false)来发送数据。

总结

这里简单介绍了kcp的整体流程,详细介绍了发送数据的流程,但未介绍kcp接收数据的流程,其实在客户端发送数据后,服务端是需要返回ack的,而客户端也需要根据返回的ack来判断数据段是否需要重传还是在队列里清除该数据段。处理返回来的ack是在函数kcp.Input()函数实现的。具体详细流程下次再介绍。

转载于:https://www.cnblogs.com/zhangboyu/p/34c07c3577c85e9ae5c3477d7cab5f52.html

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

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

相关文章

scada系统集成_MES/MOM与WMS系统集成应用案例-系统接口、交互数据分析

写在面前(By 小智)前面我们介绍了西门子、罗克韦尔、施耐德、达索等的MES/MOM平台(见文末推荐)也绍和分析了MES与ERP之间的系统集成案例&#xff1a;MES与EPR进行系统集成的实际案例-系统接口、交互数据分析同时分享了各种系统之间集成的文章&#xff1a;Teamcenter、TIA Porta…

java环境变量设置详解_JAVA环境变量配置详解(Windows)

JAVA环境变量JAVA_HOME、CLASSPATH、PATH设置详解Windows下JAVA用到的环境变量主要有3个&#xff0c;JAVA_HOME、CLASSPATH、PATH。JAVA_HOME指向的是JDK的安装路径&#xff0c;如C:\jdk1.5.0_06&#xff0c;在这路径下你应该能够找到bin、lib等目录。( 今晚使用新系统配置&…

【BZOJ1924】【SDOI2010】所驼门王的宝藏(Tarjan,SPFA)

题目描述 在宽广的非洲荒漠中&#xff0c;生活着一群勤劳勇敢的羊驼家族。被族人恭称为“先知”的Alpaca L. Sotomon是这个家族的领袖&#xff0c;外人也称其为“所驼门王”。所驼门王毕生致力于维护家族的安定与和谐&#xff0c;他曾亲自率军粉碎河蟹帝国主义的野蛮侵略&#…

ftl 展示图片_视频号变迁的内容展示逻辑

最初的视频号只有单列展示&#xff0c;只有关注栏&#xff0c;并没有朋友和热门区分。产出内容限制为不超过 1 分钟的视频&#xff0c;或 9 张以内的图片。单列信息流、时间倒序排列(也存在个别特例)。这里想到之前龙哥说的关于朋友圈的思考&#xff1a;朋友圈是一个广场&#…

10.3 考试 (考得不好)

T1 我只能说 它是一个比较暴力的dp&#xff0c;需要人力讨论... 所以考试觉得讨论太麻烦&#xff0c;直接内心崩溃了....(好像这也是我考炸的原因吧) 教训&#xff1a;以后要勤快一些&#xff0c;代码能力 唉唉唉 #include <cstdio> #include <cstring> #include &…

java接口文件定义类_Java入门笔记(四)类、包和接口

一、类 classpublic class Person{String name;int age;Person(String name, int age){this.name name;this.age age;}void sayHello(){System.out.println("Hello!");}}1. 属性a. 字段也就是成员变量&#xff0c;在构造方法中进行初始化&#xff0c;没有指针的概念…

小程序[渲染层网络层错误] failed to load image_游戏中水的渲染技术

水的渲染一直是图形学需要解决的问题&#xff0c;本篇博客主要介绍用傅里叶变换算法实现的水反射&#xff0c;也是一种假反射效果&#xff0c;目的是优化效率。实现的效果如下图所示&#xff1a;使用傅里叶系数来表示地形高度的假反射效果&#xff0c;在我们开发的游戏中使用水…

pip install 报错UnicodeDecodeError: 'ascii' codec can't decode byte 0xb5 in

当python在window环境中通过pip安装pandas报标题这样的错&#xff0c;主要是因为python默认编码格式是&#xff1a;ascii 在https://www.python.org/dev/peps/pep-0100/文章中有介绍 解决方法&#xff1a;在python/lib/site.py中加入 import sysreload(sys)sys.setdefaultenc…

jdbc是java语言编写的类和接口_JDBC——Java语言连接数据库的标准

JDBC概述APIJDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API&#xff0c;可以为多种关系数据库提供统一访问&#xff0c;它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准&#xff0c;据此可以构建更高级的工具和接口&#xff0c;使…

怎样判断电饭锅温度传感器的好坏?_空调温度传感器的作用与检测。

温度传感器是指对温度进行感应&#xff0c;并将感应的温度变化情况转换为电信号的功能部件。我们在练习温度传感器检测代换之前&#xff0c;要先对温度传感器的安装位置、结构特点和工作原理有一定的了解。1、温度传感器的安装位置及结构在空调器室内机中&#xff0c;通常设有两…

【codeforces 507E】Breaking Good

【题目链接】:https://vjudge.net/contest/164884#problem/D 【题意】 给你一张图; 图中有些路是完好的;但有些路还没修好; 先不管路有没有修好; 问你从起点到终点的最短路; 如果最短路上有没修好的路,那么你要把它修好; 而不在最短路上的,如果是完好的路,你需要把它摧毁…

java去掉图片边框颜色_sharp 去除图片边框

trim接受参数是颜色相似度, 并不能指定边框颜色, 所以有可能会误删除内容, 需要注意trim 需要转为buffer或者文件后再次操作, 否则图片信息不变从剪裁效果来看, 精度和速度都是可以的const sharp require("sharp");const path "./html/a.png";const outp…

java mail 不用密码_iPhone 无需越狱,简单给 App 加密码锁

在手机上总有些小秘密&#xff0c;也有些APP不想被打开。由于iOS本身限制&#xff0c;想给APP加上密码锁&#xff0c;一般都是需要越狱才能实现。其实在iOS 12就加入了【屏幕使用时间】&#xff0c;用来更好设置APP使用时间的限额。屏幕使用时间屏幕时间功能可以协助您了解自己…

Java植物名录程序_程序员用Java语言编写多线程应用程序,程序员能控制的关键性工作有两个方面:一是编写线程的_________方法;二是建立线程实例。...

沟通的目的是打造“3G团队”&#xff0c;其中的“3G”具体指&#xff1a;“啊,时间过得真快啊!”中的“啊”活塞与气缸盖、气缸壁共同组成燃烧室,承受气缸中气体的压力,并将此压力通过活塞销和连杆传给食品是指人食用或饮用的成品和原料,以及按照传统既是食品又是( )的物品,但不…

【t090】吉祥数

Time Limit: 1 second Memory Limit: 128 MB 【问题描述】 为了迎接圣诞&#xff0c;信息学兴趣小组的同学在辅导老师的带领下&#xff0c;举办了一个盛大的晚会&#xff0c;晚会的第一项内容是做游戏&#xff1a;猜数。老师给 每位同学发一张卡片&#xff0c;每张卡片上都有…

spark内存溢出怎么解决_和平精英:更新需要预留6G内存,玩家抱怨手机扛不住,怎么解决?...

嗨&#xff0c;小伙伴们大家好呀&#xff01;《和平精英》新版本终于来了哈&#xff0c;小伙伴们在22号上午登录游戏&#xff0c;就可以收到更新提示&#xff0c;点击进行更新就好了。或者是在各个应用商店里等待更新推送&#xff0c;也行哦&#xff01;但是呢&#xff0c;每到…

php 字符串0转换bool_PHP数据类型转换(转)

PHP数据类型转换PHP的数据类型转换属于强制转换&#xff0c;允许转换的PHP数据类型有&#xff1a;•(int)、(integer)&#xff1a;转换成整形•(float)、(double)、(real)&#xff1a;转换成浮点型•(string)&#xff1a;转换成字符串•(bool)、(boolean)&#xff1a;转换成布尔…

【转】LDA数学八卦

转自LDA数学八卦 在 Machine Learning 中&#xff0c;LDA 是两个常用模型的简称&#xff1a; Linear Discriminant Analysis 和 Latent Dirichlet Allocation&#xff0c; 在这篇文章中我们主要八卦的是后者。LDA 是一个在文本建模中很著名的模型&#xff0c;类似于 SVD, PLSA …

python3安装json库-python库json快速入门

在本教程中&#xff0c;您将学习如何借助示例在Python中解析&#xff0c;读取和编写JSON。 此外&#xff0c;您将学习将JSON转换为dict并将其打印出来。 JSON简介 JSON (JavaScript Object Notation) 是一种用于表示结构化数据的流行数据格式。 常用于服务器和Web应用程序之间传…

python 常量 模块_Python字符串模块的有用常量

示例Python的string模块为与字符串相关的操作提供常量。要使用它们&#xff0c;请导入string模块&#xff1a;>>> import stringstring.ascii_letters&#xff1a;ascii_lowercase和的串联ascii_uppercase&#xff1a;>>> string.ascii_lettersabcdefghijklm…