最近学习Spark Streaming,不知道是不是我搜索的姿势不对,总找不到具体的、完整的例子,一怒之下就决定自己写一个出来。下面以预测股票走势为例,总结了用Spark Streaming开发的具体步骤以及方法。

  一、数据源。

  既然预测股票走势,当然要从网上找一下股票数据的接口,具体可以参考 http://blog.sina.com.cn/s/blog_540f22560100ba2k.htmlhttp://apistore.baidu.com/apiworks/servicedetail/115.html 。下面简单分析一下各种数据接口的优劣以抛砖引玉:

  1、Sina股票数据接口。以字符串数据的形式范围,简单易用且直观。

  2、百度数据接口。以API集市形式提供json形式的数据,比较规范,但使用起来比较繁琐。

  简单起见,作者使用新浪的数据接口。

  二、测试数据源

  有了股票的数据接口,以下代码提供简单的测试,以解析返回的数据。

  1. /**
  2. * Created by gabry.wu on 2016/2/18.
  3. */
  4. package com.gabry.stock
  5.  
  6. import scala.io.Source
  7. /** 其实这个类应该更通用一点,但目前一切以简单为主,后期在进行重构 **/
  8. class SinaStock
  9. {
  10. var code:String="" //“sh601006”,股票代码
  11. var name :String ="" //”大秦铁路”,股票名字
  12. var curOpenPrice :Float =0 //”27.55″,今日开盘价
  13. var lstOpenPrice:Float =0 //”27.25″,昨日收盘价
  14. var curPrice :Float =0 //”26.91″,当前价格
  15. var highestPrice :Float =0 //”27.55″,今日最高价
  16. var lowestPrice :Float=0 //”26.20″,今日最低价
  17. var bidBuyPrice:Float=0 //”26.91″,竞买价,即“买一”报价
  18. var bidSalePrice:Float=0 //”26.92″,竞卖价,即“卖一”报价
  19. var dealNum :Long=0 //8:”22114263″,成交的股票数,由于股票交易以一百股为基本单位,所以在使用时,通常把该值除以一百
  20. var dealAmount :Float=0 //9:”589824680″,成交金额,单位为“元”,为了一目了然,通常以“万元”为成交金额的单位,所以通常把该值除以一万
  21. var bidBuy1Num :Long=0 //10:”4695″,“买一”申请4695股,即47手
  22. var bidBuy1Amount :Float=0 //11:”26.91″,“买一”报价
  23. var bidBuy2Num :Long=0
  24. var bidBuy2Amount :Float=0
  25. var bidBuy3Num :Long=0
  26. var bidBuy3Amount :Float=0
  27. var bidBuy4Num :Long=0
  28. var bidBuy4Amount :Float=0
  29. var bidBuy5Num :Long=0
  30. var bidBuy5Amount :Float=0
  31. var bidSale1Num :Long=0 //“卖一”申报3100股,即31手
  32. var bidSale1Amount :Float=0 //“卖一”报价
  33. var bidSale2Num :Long=0
  34. var bidSale2Amount :Float=0
  35. var bidSale3Num :Long=0
  36. var bidSale3Amount :Float=0
  37. var bidSale4Num :Long=0
  38. var bidSale4Amount :Float=0
  39. var bidSale5Num :Long=0
  40. var bidSale5Amount :Float=0
  41. var date:String ="" //”2008-01-11″,日期
  42. var time:String="" //”15:05:32″,时间
  43. def toDebugString = "code[%s],name[%s],curOpenPrice [%f],lstOpenPrice[%f],curPrice [%f],highestPrice [%f],lowestPrice [%f],bidBuyPrice[%f],bidSalePrice[%f],dealNum [%d],dealAmount [%f],bidBuy1Num [%d],bidBuy1Amount [%f],,bidBuy2Num [%d],bidBuy2Amount [%f],bidBuy3Num [%d],bidBuy3Amount [%f],bidBuy4Num [%d],bidBuy4Amount [%f],bidBuy5Num [%d],bidBuy5Amount [%f],bidSale1Num [%d],bidSale1Amount [%f],bidSale2Num [%d],bidSale2Amount [%f],bidSale3Num [%d],bidSale3Amount [%f],bidSale4Num [%d],bidSale4Amount [%f],bidSale5Num [%d],bidSale5Amount [%f],date [%s],time [%s]" .format( this.code, this.name, this.curOpenPrice , this.lstOpenPrice, this.curPrice , this.highestPrice , this.lowestPrice , this.bidBuyPrice, this.bidSalePrice, this.dealNum , this.dealAmount , this.bidBuy1Num , this.bidBuy1Amount , this.bidBuy2Num , this.bidBuy2Amount , this.bidBuy3Num , this.bidBuy3Amount , this.bidBuy4Num , this.bidBuy4Amount , this.bidBuy5Num , this.bidBuy5Amount , this.bidSale1Num , this.bidSale1Amount , this.bidSale2Num , this.bidSale2Amount , this.bidSale3Num , this.bidSale3Amount , this.bidSale4Num , this.bidSale4Amount , this.bidSale5Num , this.bidSale5Amount , this.date , this.time )
  44. override def toString = Array(this.code,this.name,this.curOpenPrice,this.lstOpenPrice,this.curPrice,this.highestPrice,this.lowestPrice,this.bidBuyPrice,this.bidSalePrice,this.dealNum,this.dealAmount,this.bidBuy1Num,this.bidBuy1Amount,this.bidBuy2Num,this.bidBuy2Amount,this.bidBuy3Num,this.bidBuy3Amount,this.bidBuy4Num,this.bidBuy4Amount,this.bidBuy5Num,this.bidBuy5Amount,this.bidSale1Num,this.bidSale1Amount,this.bidSale2Num,this.bidSale2Amount,this.bidSale3Num,this.bidSale3Amount,this.bidSale4Num,this.bidSale4Amount,this.bidSale5Num,this.bidSale5Amount,this.date,this.time).mkString(",")
  45. private var stockInfo :String =""
  46. def getStockInfo = stockInfo
  47. def this(stockInfo:String)
  48. {
  49. this()
  50. this.stockInfo=stockInfo
    /** 根据新浪的数据接口解析数据 **/
  51. val stockDetail=stockInfo.split(Array(' ','_','=',',','"'))
  52. if (stockDetail.length>36){
  53. this.code=stockDetail(3)
  54. this.name=stockDetail(5)
  55. this.curOpenPrice =stockDetail(6).toFloat
  56. this.lstOpenPrice=stockDetail(7).toFloat
  57. this.curPrice =stockDetail(8).toFloat
  58. this.highestPrice =stockDetail(9).toFloat
  59. this.lowestPrice =stockDetail(10).toFloat
  60. this.bidBuyPrice=stockDetail(11).toFloat
  61. this.bidSalePrice=stockDetail(12).toFloat
  62. this.dealNum =stockDetail(13).toLong
  63. this.dealAmount =stockDetail(14).toFloat
  64. this.bidBuy1Num =stockDetail(15).toLong
  65. this.bidBuy1Amount =stockDetail(16).toFloat
  66. this.bidBuy2Num =stockDetail(17).toLong
  67. this.bidBuy2Amount =stockDetail(18).toFloat
  68. this.bidBuy3Num =stockDetail(19).toLong
  69. this.bidBuy3Amount =stockDetail(20).toFloat
  70. this.bidBuy4Num =stockDetail(21).toLong
  71. this.bidBuy4Amount =stockDetail(22).toFloat
  72. this.bidBuy5Num =stockDetail(23).toLong
  73. this.bidBuy5Amount =stockDetail(24).toFloat
  74. this.bidSale1Num =stockDetail(25).toLong
  75. this.bidSale1Amount =stockDetail(26).toFloat
  76. this.bidSale2Num =stockDetail(27).toLong
  77. this.bidSale2Amount =stockDetail(28).toFloat
  78. this.bidSale3Num =stockDetail(29).toLong
  79. this.bidSale3Amount =stockDetail(30).toFloat
  80. this.bidSale4Num =stockDetail(31).toLong
  81. this.bidSale4Amount =stockDetail(32).toFloat
  82. this.bidSale5Num =stockDetail(33).toLong
  83. this.bidSale5Amount =stockDetail(34).toFloat
  84. this.date =stockDetail(35)
  85. this.time =stockDetail(36)
  86. }
  87. }
  88. }
    /** SinaStock的伴生对象,此处用来替代new **/
  89. object SinaStock
  90. {
  91. def apply(stockInfo:String) :SinaStock =
  92. {
  93. new SinaStock(stockInfo)
  94. }
  95. }
  96. object StockRetrivor {
  97. def main(args: Array[String]): Unit = {
  98. println("查询新浪股票(每小时更新) http://hq.sinajs.cn/list=sh601006,sh601007")
    /** 查询sh601006,sh601007两只股票 **/
  99. val sinaStockStream = Source.fromURL("http://hq.sinajs.cn/list=sh601006,sh601007","gbk")
  100. val sinaLines=sinaStockStream.getLines
  101. for(line <- sinaLines) {
    /** 将每行数据解析成SinaStock对象,并答应对应的股票信息 **/
  102. println(SinaStock(line).toString)
  103. }
  104. sinaStockStream.close()
  105. }
  106. }

  三、Spark Streaming编程

  数据接口调试完毕,股票数据也解析好了,下面就开始Streaming。Spark Streaming一定会涉及数据源,且该数据源是一个主动推送的过程,即spark被动接受该数据源的数据进行分析。但Sina的接口是一个很简单的HttpResponse,无法主动推送数据,所以我们需要实现一个Custom Receiver,可参考 http://spark.apache.org/docs/latest/streaming-custom-receivers.html

  下面是具体的代码,其实定制化一个Receiver简单来说就是实现onStart/onStop。onStart用来初始化资源,给获取数据做准备,获取到的数据用store发送给SparkStreaming即可;onStop用来释放资源

  1. package com.gabry.stock
  2.  
  3. import org.apache.spark.Logging
  4. import org.apache.spark.storage.StorageLevel
  5. import org.apache.spark.streaming.receiver.Receiver
  6.  
  7. import scala.io.Source
  8.  
  9. /**
  10. * Created by gabry.wu on 2016/2/19.
  11. * 简单起见,只获取新浪股票数据,后续再进行重构
  12. */
  13. class SinaStockReceiver extends Receiver[String](StorageLevel.MEMORY_AND_DISK_2) with Logging{
  14. def onStart() {
  15. /* 创建一个线程用来查询新浪股票数据,并将数据发送给Spark Streaming */
  16. new Thread("Socket Receiver") {
  17. override def run() { receive() }
  18. }.start()
  19. }
  20.  
  21. def onStop() {
  22. // There is nothing much to do as the thread calling receive()
  23. // is designed to stop by itself isStopped() returns false
  24. }
  25. private def receive(): Unit = {
  26. try{
  27. while(!isStopped ) {
  28. var stockIndex = 1
  29. while(stockIndex!=0){
  30. val stockCode = 601000+stockIndex
  31. val url="http://hq.sinajs.cn/list=sh%d".format(stockCode)
  32. logInfo(url)
  33. val sinaStockStream = Source.fromURL(url,"gbk")
  34. val sinaLines=sinaStockStream.getLines
  35. for(line <- sinaLines) {
  36. logInfo(line)
  37. store(line)
  38. }
  39. sinaStockStream.close()
  40. stockIndex= (stockIndex+1)%1
  41. }
  42.  
  43. }
  44.  
  45. logInfo("Stopped receiving")
  46. restart("Trying to connect again")
  47. } catch {
  48. case e: java.net.ConnectException =>
  49. restart("Error connecting to", e)
  50. case t: Throwable =>
  51. restart("Error receiving data", t)
  52. }
  53. }
  54. }

  Receiver搞定之后就可以开始编写股票预测的main函数了,贴代码之前说明一下,股票预测的方法之一,就是统计一段时间内股票上涨的次数,并展示上涨次数TopN的股票信息,但本文一切从简,并没有实现全部的功能,只是统计了股票上涨的次数,也就是对上涨与否进行WordCount。

  1. /**
  2. * Created by gabry.wu on 2016/2/19.
  3. */
  4. package com.gabry.stock
  5.  
  6. import org.apache.log4j.{Level, Logger}
  7. import org.apache.spark.{HashPartitioner, SparkConf}
  8. import org.apache.spark.streaming.{Seconds, StreamingContext}
  9.  
  10. object StockTrend {
  11. def updatePriceTrend( newValue:Seq[(Float,Int)],preValue :Option[(Float,Int)]):Option[(Float,Int)] = {
  12. if (newValue.length>0){
  13. val priceDiff=newValue(0)._1 - preValue.getOrElse((newValue(0)._1 ,0))._1
  14. // ("update state: new Value "+newValue(0) + ",pre Value " + preValue.getOrElse((newValue(0)._1 ,0)))
  15. Some((newValue(0)._1,priceDiff.compareTo(0.0f)))
  16. }else preValue
  17. }
  18.  
  19. def main(args: Array[String]): Unit = {
  20. val sparkConf = new SparkConf().setAppName("CustomReceiver").setMaster("local[4]")
  21. val ssc = new StreamingContext(sparkConf, Seconds(1))
  22. Logger.getRootLogger.setLevel(Level.WARN)
  23. ssc.checkpoint("./tmp")
  24. /* 创建股票的输入流,该输入流是自定义的 */
  25. val lines = ssc.receiverStream(new SinaStockReceiver())
    /** 将数据的每一行映射成一个SinaStock对象。注意此处的每一行数据都是SinaStockReceiver对象调用store传过来的 **/
  26. val words = lines.map(SinaStock(_))
  27. import scala.util.Random
  28. /* reduce从左到右进行折叠。其实就是先处理t-6,t-5的RDD,将结果与t-4的RDD再次调用reduceFunc,依次类推直到当前RDD */
  29. def reduceFunc( left :(Float,Int),right:(Float,Int)):(Float,Int) = {
  30. println("left "+left+"right "+right)
  31. (right._1,left._2+right._2)
  32. }
  33.  
  34. /* 3点之后股票价格不在变化,故为了测试,此处使用随机数修改股票当前价格 */
  35. /* 根据上一次股票价格更新股票的变化方向 */
    /** 由于股票信息只有当前价格,如果要判断股票上涨与否就要记录上一次的股票价格,所以此处使用updateStateByKey更新当前股票价格是否上涨。
        若上涨则记为1,不变记为0,否则记为1
    **/
  36. val stockState = words.map(sinaStock => (sinaStock.name, (sinaStock.curPrice+Random.nextFloat,-1))).filter(stock=>stock._1.isEmpty==false)
    .updateStateByKey(updatePriceTrend)
  37. /* 每3秒,处理过去6秒的数据,对数据进行变化的累加 */
  38. val stockTrend=stockState.reduceByKeyAndWindow(reduceFunc(_,_),Seconds(6),Seconds(3))
  39. /* 每3秒,处理过去6秒的数据,对数据进行正向变化的累加 */
  40. //val stockPosTrend=stockState.filter(x=>x._2._2>=0).reduceByKeyAndWindow(reduceFunc(_,_),Seconds(6),Seconds(3))
  41. stockState.print()
  42. stockTrend.print()
  43. //stockPosTrend.print()
  44. ssc.start()
  45. ssc.awaitTermination()
  46. println("StockTrend")
  47. }
  48. }

  四、运行结果分析

  下面是某次运行的打印结果,对其进行简单的分析。

  由于ssc的时间间隔为1,所以每秒都会查询大同煤业的股票数据,这就是下面每个Time打印的第一行数据(因为stockState先进行print,所以每次查询的股票数据是第一行);又因为slide设置为3,所以每隔3秒会进行reduceFunc计算,该函数处理windowsize个RDD(此处设置为6),对这6个RDD按照时间先后顺序进行reduce。

  需要特别说明的是spark的reduce默认从左到右进行fold(折叠),从最左边取两个数进行reduce计算产生临时结果,再与后面的数据进行reduce,以此类推进行计算,其实就是foldLeft。

  下面标红色的数据,其实就是对(5.387682,0),(5.9087195,1),(5.7605586,-1),(5.278526,-1),(5.4471517,1),(5.749305,1)进行reduce的过程。

-------------------------------------------

Time: 1455888254000 ms

-------------------------------------------

(大同煤业,(5.387682,0))

-------------------------------------------

Time: 1455888255000 ms

-------------------------------------------

(大同煤业,(5.9087195,1))

-------------------------------------------

Time: 1455888256000 ms

-------------------------------------------

(大同煤业,(5.7605586,-1))

left (5.387682,0)right (5.9087195,1)

left (5.9087195,1)right (5.7605586,-1)

-------------------------------------------

Time: 1455888256000 ms

-------------------------------------------

(大同煤业,(5.7605586,0))

-------------------------------------------

Time: 1455888257000 ms

-------------------------------------------

(大同煤业,(5.278526,-1))

-------------------------------------------

Time: 1455888258000 ms

-------------------------------------------

(大同煤业,(5.4471517,1))

-------------------------------------------

Time: 1455888259000 ms

-------------------------------------------

(大同煤业,(5.749305,1))

left (5.387682,0)right (5.9087195,1)

left (5.9087195,1)right (5.7605586,-1)

left (5.7605586,0)right (5.278526,-1)

left (5.278526,-1)right (5.4471517,1)

left (5.4471517,0)right (5.749305,1)

-------------------------------------------

Time: 1455888259000 ms

-------------------------------------------

(大同煤业,(5.749305,1))

-------------------------------------------

Time: 1455888260000 ms

-------------------------------------------

(大同煤业,(5.749305,1))

-------------------------------------------

Time: 1455888261000 ms

-------------------------------------------

(大同煤业,(5.748391,-1))

-------------------------------------------

Time: 1455888262000 ms

-------------------------------------------

(大同煤业,(5.395269,-1))

left (5.278526,-1)right (5.4471517,1)

left (5.4471517,0)right (5.749305,1)

left (5.749305,1)right (5.749305,1)

left (5.749305,2)right (5.748391,-1)

left (5.748391,1)right (5.395269,-1)

-------------------------------------------

Time: 1455888262000 ms

-------------------------------------------

(大同煤业,(5.395269,0))

-------------------------------------------

Time: 1455888263000 ms

-------------------------------------------

(大同煤业,(5.5215807,1))

-------------------------------------------

Time: 1455888264000 ms

-------------------------------------------

(大同煤业,(5.945005,1))

-------------------------------------------

Time: 1455888265000 ms

-------------------------------------------

(大同煤业,(5.2400274,-1))

left (5.749305,1)right (5.748391,-1)

left (5.748391,0)right (5.395269,-1)

left (5.395269,-1)right (5.5215807,1)

left (5.5215807,0)right (5.945005,1)

left (5.945005,1)right (5.2400274,-1)

-------------------------------------------

Time: 1455888265000 ms

-------------------------------------------

(大同煤业,(5.2400274,0))

-------------------------------------------

Time: 1455888266000 ms

-------------------------------------------

(大同煤业,(5.1895638,-1))

-------------------------------------------

Time: 1455888267000 ms

-------------------------------------------

(大同煤业,(5.1885605,-1))

-------------------------------------------

Time: 1455888268000 ms

-------------------------------------------

(大同煤业,(5.9881735,1))

Process finished with exit code -1

  五、总结

  本文以股票预测为例简单描述了SparkStreaming编程的步骤及其注意点,希望抛砖引玉,也算弥补了网上没有完整例子的遗憾。但由于作者重代码、轻描述,估计会有一些不易理解的地方,还望各位读者留言讨论。最后附上源码的git地址:http://git.oschina.net/gabry_wu/BigDataPractice

PS:未经允许,禁止转载,否则将追究法律责任!

基于Spark Streaming预测股票走势的例子(一)的更多相关文章

  1. 基于Spark Streaming预测股票走势的例子(二)

    上一篇博客中,已经对股票预测的例子做了简单的讲解,下面对其中的几个关键的技术点再作一些总结. 1.updateStateByKey 由于在1.6版本中有一个替代函数,据说效率比较高,所以作者就顺便研究 ...

  2. 苏宁基于Spark Streaming的实时日志分析系统实践 Spark Streaming 在数据平台日志解析功能的应用

    https://mp.weixin.qq.com/s/KPTM02-ICt72_7ZdRZIHBA 苏宁基于Spark Streaming的实时日志分析系统实践 原创: AI+落地实践 AI前线 20 ...

  3. StreamDM:基于Spark Streaming、支持在线学习的流式分析算法引擎

    StreamDM:基于Spark Streaming.支持在线学习的流式分析算法引擎 streamDM:Data Mining for Spark Streaming,华为诺亚方舟实验室开源了业界第一 ...

  4. 基于Spark Streaming + Canal + Kafka对Mysql增量数据实时进行监测分析

    Spark Streaming可以用于实时流项目的开发,实时流项目的数据源除了可以来源于日志.文件.网络端口等,常常也有这种需求,那就是实时分析处理MySQL中的增量数据.面对这种需求当然我们可以通过 ...

  5. Spark Streaming 002 统计单词的例子

    1.准备 事先在hdfs上创建两个目录: 保存上传数据的目录:hdfs://alamps:9000/library/SparkStreaming/data checkpoint的目录:hdfs://a ...

  6. Spark 实践——基于 Spark Streaming 的实时日志分析系统

    本文基于<Spark 最佳实践>第6章 Spark 流式计算. 我们知道网站用户访问流量是不间断的,基于网站的访问日志,即 Web log 分析是典型的流式实时计算应用场景.比如百度统计, ...

  7. 通过机器学习的线性回归算法预测股票走势(用Python实现)

    在本人的新书里,将通过股票案例讲述Python知识点,让大家在学习Python的同时还能掌握相关的股票知识,所谓一举两得.这里给出以线性回归算法预测股票的案例,以此讲述通过Python的sklearn ...

  8. 【自动化】基于Spark streaming的SQL服务实时自动化运维

    设计背景 spark thriftserver目前线上有10个实例,以往通过监控端口存活的方式很不准确,当出故障时进程不退出情况很多,而手动去查看日志再重启处理服务这个过程很低效,故设计利用Spark ...

  9. 一个spark streaming的黑名单过滤小例子

    > nc -lk 9999 20190912,sz 20190913,lin package com.lin.spark.streaming import org.apache.spark.Sp ...

随机推荐

  1. UVA - 1611 Crane (思路题)

    题目: 输入一个1~n(1≤n≤300)的排列,用不超过96次操作把它变成升序.每次操作都可以选一个长度为偶数的连续区间,交换前一半和后一半.输出每次操作选择的区间的第一个和最后一个元素. 思路: 注 ...

  2. js之循环语句

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. 为什么要有uboot?带你全面分析嵌入式linux系统启动过程中uboot的作用

    1.为什么要有uboot 1.1.计算机系统的主要部件 (1)计算机系统就是以CPU为核心来运行的系统.典型的计算机系统有:PC机(台式机+笔记本).嵌入式设备(手机.平板电脑.游戏机).单片机(家用 ...

  4. 数据库中间件MyCat学习总结(1)——MyCat入门简介

    为什么需要MyCat? 虽然云计算时代,传统数据库存在着先天性的弊端,但是NoSQL数据库又无法将其替代.如果传统数据易于扩展,可切分,就可以避免单机(单库)的性能缺陷. MyCat的目标就是:低成本 ...

  5. 通过混合编程分析的方法和机器学习预测Web应用程序的漏洞

    通过混合编程分析的方法和机器学习预测Web应用程序的漏洞 由于时间和资源的限制,web软件工程师需要支持识别出有漏洞的代码.一个实用的方法用来预测漏洞代码可以提高他们安全审计的工作效率.在这篇文章中, ...

  6. nyoj 93 汉诺塔(三)(stack)

    汉诺塔(三) 时间限制:3000 ms  |  内存限制:65535 KB 难度:3   描述 在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针.印度 ...

  7. asp.net mvc 4.0 新特性之移动特性

    asp.net mvc 4.0 新特性之移动特性 为不同的客户端提供不同的视图 手动重写 UserAgent,从而强制使用对应的视图 示例1.演示如何为不同的客户端提供不同的视图Global.asax ...

  8. noip模拟赛 水题

    题目描述 LYK出了道水题. 这个水题是这样的:有两副牌,每副牌都有n张. 对于第一副牌的每张牌长和宽分别是xi和yi.对于第二副牌的每张牌长和宽分别是aj和bj.第一副牌的第i张牌能覆盖第二副牌的第 ...

  9. Ubuntu 16.04下UML建模PowerDesigner的替代ERMaster和MySQL Workbench

    ERMaster是Eclipse的一个插件,小巧,支持连接各种数据库,还能生成代码等.安装参考:http://www.cnblogs.com/EasonJim/p/6170686.html 当然还有一 ...

  10. 如何基于udp实现tcp协议栈

    http://bbs.csdn.net/topics/280046868 使用套接字完成,按照tcp的方式在一个套接字里维持一个状态机. //定义枚举: enmu state{ CLOSED,//没有 ...