这篇文章主要使用Go语言实现一个简单的TCP服务器和客户端。
服务器和客户端之间的协议是 ECHO, 这个RFC 862定义的一个简单协议。
为什么说这个协议很简单呢, 这是因为服务器只需把收到的客户端的请求数据发给这个客户端即可,其它什么功能都不做。

首先声明, 我绝对是一个Golang的初学者,十四、五年的编程时间我主要使用Java来做开发,这篇文章主要记录我学习go网络编程的体验。如果你认为这篇文章有错误或者不好的写法,请在回复中添加你的意见。

简单介绍

尽管OSI(开放系统互联)协议从未被完整地实现过,但它仍对分布式系统的讨论和设计产生了十分重要的影响。它的结构大致为下图所示:

当OSI标准模型正在为实现细节闹得不可开交时,DARPA互联网技术项目却在忙着构建TCP/IP协议。它们取得了极大的成功,并引领了Internet(首字母大写),因为这是个更简单的层次结构:

尽管现在到处都是TCP/IP协议,但它并不是唯一存在的。还有些协议占有重要的地位,比如:

  • Firewire
  • USB
  • Bluetooth
  • WiFi

多年的发展,使得IP和TCP/UDP协议基本上就等价于网络协议栈。例如, 蓝牙定义了物理层和协议层,但在其上任然是IP协议栈,可以在许多蓝牙设备之间使用互联网编程技术。同样, 开发4G无线手机技术,如LTE(Long Term Evolution)也将使用IP协议栈。

在OIS或TCP/IP协议栈层与层之间的通信,是通过将数据包从一个层发送到下一个层,最终穿过整个网络的。每一层都有必须保持其自身层的管理信息。从上层接收到的数据包在向下传递时,会添加头信息。在接收端,这些头信息会在向上传递时移除。
例如,TFTP(普通文件传输协议)将文件从一台计算机移动到另一台上。它使用IP协议上的UDP协议,该协议可通过以太网发送。看起来就像这样

最终在以太网上传输的数据,就是图中最底层的那个数据。

为了两个计算机进行通信,就必须建立一个路径,使他们能够在一个会话中发送至少一条消息。有两个主要的模型:

  • 面向连接模型, 如TCP
  • 无连接模型, 如UDP, IP

服务运行在主机。通常它们的生命期很长,同时被设计成等待请求和响应请求。当前有各种类型的服务,通过各种方法向客户提供服务。互联网的世界基于TCP和UDP这两种通信方法提供许多这些服务,虽然也有其他通信协议如SCTP​​伺机取代。许多其他类型的服务,例如点对点, 远过程调用, 通信代理, 和许多其他也建立在TCP和UDP之上。

服务存活于主机内。IP地址可以定位主机。但在每台计算机上可能会提供多种服务,需要一个简单的方法对它们加以区分。TCP,UDP,SCTP或者其他协议使用端口号来加以区分。这里使用一个1到65,535的无符号整数,每个服务将这些端口号中的一个或多个相关联。

有很多“标准”的端口。Telnet服务通常使用端口号23的TCP协议。DNS使用端口号53的TCP或UDP协议。FTP使用端口21和20的命令,进行数据传输。HTTP通常使用端口80,但经常使用,端口8000,8080和8088,协议为TCP。X Window系统往往需要端口6000-6007,TCP和UDP协议。

在Unix系统中, /etc/services文件列出了常用的端口。Go语言有一个函数可以获取该文件。

1
func LookupPort(network, service string) (port int, err os.Error)

Go提供IP, IP掩码, TCPAddr, UDPAddr, 网卡,主机查询这些对象的操作函数。

当你知道如何通过网络和端口ID查找一个服务时,然后呢?如果你是一个客户端,你需要一个API,让您连接到服务,然后将消息发送到该服务,并从服务读取回复。
如果你是一个服务器,你需要能够绑定到一个端口,并监听它。当有消息到来,你需要能够读取它并回复客户端。

net.TCPConn是允许在TCP客户端和TCP服务器之间的全双工通信的Go类型。两种主要方法是

1
2
func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)

TCPConn被客户端和服务器用来读写消息。

ECHO服务器

在一个服务器上注册并监听一个端口。然后它阻塞在一个"accept"操作,并等待客户端连接。当一个客户端连接, accept调用返回一个连接(connection)对象。ECHO服务非常简单,只是客户端, 关闭该连接的请求数据写回到客户端,就像回声一样,直到某一方关闭连接。

1
2
func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)

net参数可以设置为字符串"tcp", "tcp4"或者"tcp6"中的一个。如果你想监听所有网络接口,IP地址应设置为0。 如果你只是想监听一个特定网络接口,IP地址可以设置为该网络接口的地址。如果端口设置为0,操作系统会为你选择一个端口。否则,你可以选择你自己的。需要注意的是,在Unix系统上,除非你是监控系统,否则不能监听低于1024的端口,小于128的端口是由IETF标准化。该示例程序选择端口1200没有特别的原因。TCP地址如下":1200" - 所有网络接口, 端口1200。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main
 
import (
"flag"
"fmt"
"io"
"net"
"os"
)
 
var host = flag.String("host", "", "host")
var port = flag.String("port", "3333", "port")
 
func main() {
flag.Parse()
var l net.Listener
var err error
l, err = net.Listen("tcp", *host+":"+*port)
if err != nil {
fmt.Println("Error listening:", err)
os.Exit(1)
}
defer l.Close()
fmt.Println("Listening on " + *host + ":" + *port)
 
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err)
os.Exit(1)
}
 
//logs an incoming message
fmt.Printf("Received message %s -> %s \n", conn.RemoteAddr(), conn.LocalAddr())
 
// Handle connections in a new goroutine.
go handleRequest(conn)
}
}
 
func handleRequest(conn net.Conn) {
defer conn.Close()
 
for {
io.Copy(conn, conn)
}
 
}

执行go run echoserver.go启动服务器。

ECHO客户端

一旦客户端已经建立TCP服务, 就可以"拨号"了. 如果成功,该调用返回一个用于通信的TCPConn。客户端和服务器通过它交换消息。通常情况下,客户端使用TCPConn写入请求到服务器, 并从TCPConn的读取响应。持续如此,直到任一(或两者)的两侧关闭连接。客户端使用该函数建立一个TCP连接。

1
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)

其中laddr是本地地址,通常设置为nil。 raddr是一个服务的远程地址, net是一个字符串,可以根据你的需要设置为"tcp4", "tcp6"或"tcp"中的一个。

在介绍实现时,我们需要介绍同步机制, 因为客户端发送和接收是在两个goroutine中。 main函数中如果不加上同步机制, 客户端还没有发送接收就执行完了。
我们实现了两种同步方式。 当然还有其它方式, 如time.Sleep(60*1000)或者等待从命令行输入,不过看起来有点傻。

Go格言
Share memory by communicating, don't communicate by sharing memory

使用Channel等待goroutine完成

比如老套的方式通过channel实现同步。 读和写完成后分别往channel中写入"done"。 main读取channel中的值,当两个done都读取到后就知道读写已经完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main
 
import (
"flag"
"fmt"
"net"
"os"
"strconv"
)
 
var host = flag.String("host", "localhost", "host")
var port = flag.String("port", "3333", "port")
 
func main() {
flag.Parse()
conn, err := net.Dial("tcp", *host+":"+*port)
if err != nil {
fmt.Println("Error connecting:", err)
os.Exit(1)
}
defer conn.Close()
 
fmt.Println("Connecting to " + *host + ":" + *port)
 
done := make(chan string)
go handleWrite(conn, done)
go handleRead(conn, done)
 
fmt.Println(<-done)
fmt.Println(<-done)
}
 
func handleWrite(conn net.Conn, done chan string) {
for i := 10; i > 0; i-- {
_, e := conn.Write([]byte("hello " + strconv.Itoa(i) + "\r\n"))
 
if e != nil {
fmt.Println("Error to send message because of ", e.Error())
break
}
}
 
done <- "Sent"
}
 
func handleRead(conn net.Conn, done chan string) {
buf := make([]byte, 1024)
reqLen, err := conn.Read(buf)
if err != nil {
fmt.Println("Error to read message because of ", err)
return
}
fmt.Println(string(buf[:reqLen-1]))
 
done <- "Read"
}

net.Dial建立连接, handleWrite发送十个请求, handleRead 接收服务器的响应。一旦完成,往channel中写done。
执行go run echoclient.go启动服务器。

使用WaitGroup等待goroutine完成

上面的方式虽好,但是不够灵活。我们需要明确知道有多少个done。 如果增加若干个goroutine,修改起来比较麻烦。
所以还是使用sync包的WaitGroup比较灵活。 它类似Java中的CountDownLatch。

A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.

将上面的例子修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main
 
import (
"bufio"
"flag"
"fmt"
"net"
"os"
"strconv"
"sync"
)
 
var host = flag.String("host", "localhost", "host")
var port = flag.String("port", "3333", "port")
 
func main() {
flag.Parse()
conn, err := net.Dial("tcp", *host+":"+*port)
 
if err != nil {
fmt.Println("Error connecting:", err)
os.Exit(1)
}
defer conn.Close()
 
fmt.Println("Connecting to " + *host + ":" + *port)
 
var wg sync.WaitGroup
wg.Add(2)
 
go handleWrite(conn, &wg)
go handleRead(conn, &wg)
 
wg.Wait()
}
 
func handleWrite(conn net.Conn, wg *sync.WaitGroup) {
defer wg.Done()
 
for i := 10; i > 0; i-- {
_, e := conn.Write([]byte("hello " + strconv.Itoa(i) + "\r\n"))
 
if e != nil {
fmt.Println("Error to send message because of ", e.Error())
break
}
}
}
 
func handleRead(conn net.Conn, wg *sync.WaitGroup) {
defer wg.Done()
 
reader := bufio.NewReader(conn)
for i := 1; i <= 10; i++ {
line, err := reader.ReadString(byte('\n'))
if err != nil {
fmt.Print("Error to read message because of ", err)
return
}
fmt.Print(line)
}
}

wg.Add(2)设定等待两个goroutines, 然后调用wg.Wait()等待goroutines完成。 当goroutine完成时, 调用wg.Done()。 使用起来相当简洁。

参考

    1. http://tools.ietf.org/html/rfc862
    2. http://loige.com/simple-echo-server-written-in-go-dockerized/
    3. http://nathanleclaire.com/blog/2014/02/15/how-to-wait-for-all-goroutines-to-finish-executing-before-continuing/
    4. http://jan.newmarch.name/go/zh/
    5. https://talks.golang.org/2012/concurrency.slide
    6. http://jimmyfrasche.github.io/go-reflection-codex/
    7. https://sites.google.com/site/gopatterns/
    8. https://github.com/golang-samples
    9. http://golang-examples.tumblr.com/
    10. https://code.google.com/p/go-wiki/wiki/Articles
    11. https://github.com/mindreframer/golang-stuff

go --socket通讯(TCP服务端与客户端的实现)的更多相关文章

  1. socket创建TCP服务端和客户端

    看情况选择相对应的套接字*面向连接的传输--tcp协议--可靠的--流式套接字(SOCK_STREAM)*面向无连接的传输--udp协议--不可靠的--数据报套接字(SOCK_DGRAM) 在liun ...

  2. QTcpSocket-Qt使用Tcp通讯实现服务端和客户端

    版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:QTcpSocket-Qt使用Tcp通讯实现服务端和客户端     本文地址:https:// ...

  3. QUdpSocket-Qt使用Udp通讯实现服务端和客户端

    版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:QUdpSocket-Qt使用Udp通讯实现服务端和客户端     本文地址:https:// ...

  4. 【转】TCP/UDP简易通信框架源码,支持轻松管理多个TCP服务端(客户端)、UDP客户端

    [转]TCP/UDP简易通信框架源码,支持轻松管理多个TCP服务端(客户端).UDP客户端 目录 说明 TCP/UDP通信主要结构 管理多个Socket的解决方案 框架中TCP部分的使用 框架中UDP ...

  5. swoole创建TCP服务端和客户端

    服务端: server.php <?php //创建Server对象,监听 127.0.0.1:9501端口    $serv = new swoole_server("127.0.0 ...

  6. 基于Select模型的Windows TCP服务端和客户端程序示例

    最近跟着刘远东老师的<C++百万并发网络通信引擎架构与实现(服务端.客户端.跨平台)>,Bilibili视频地址为C++百万并发网络通信引擎架构与实现(服务端.客户端.跨平台),重新复习下 ...

  7. TCP/UDP简易通信框架源码,支持轻松管理多个TCP服务端(客户端)、UDP客户端

    目录 说明 TCP/UDP通信主要结构 管理多个Socket的解决方案 框架中TCP部分的使用 框架中UDP部分的使用 框架源码结构 补充说明 源码地址 说明 之前有好几篇博客在讲TCP/UDP通信方 ...

  8. Java TCP服务端向客户端发送图片

    /** * 1.创建TCP服务端,TCP客户端 * 2.服务端等待客户端连接,客户端连接后,服务端向客户端写入图片 * 3.客户端收到后进行文件保存 * @author Administrator * ...

  9. python创建tcp服务端和客户端

    1.tcp服务端server from socket import * from time import ctime HOST = '' PORT = 9999 BUFSIZ = 1024 ADDR ...

随机推荐

  1. Careercup - Microsoft面试题 - 24308662

    2014-05-12 07:31 题目链接 原题: I have heard this question many times in microsoft interviews. Given two a ...

  2. 设计模式之第12章-享元模式(Java实现)

    设计模式之第12章-享元模式(Java实现) “怎么回事,竟然出现了OutOfMemory的错误.鱼哥,来帮我看看啊.”“有跟踪错误原因么?是内存泄露么?”“不是内存泄露啊,具体原因不知道啊.对了,有 ...

  3. 6、CSS基础 part-4

    1.CSS 定位属性 CSS 定位属性允许你对元素进行定位. 属性 描述 position 把元素放置到一个静态的.相对的.绝对的.或固定的位置中. top 定义了一个定位元素的上外边距边界与其包含块 ...

  4. 【数据结构与算法】Fibonacci Sequence

    学计算机的对 Fibonacci 都并不陌生,在课堂上一讲到递归几乎都会提到 Fibonacci 数列.不久前,我对 Fibonacci 产生了一些兴趣,就在这里把自己的想法给记录下来. 递推公式: ...

  5. 【bzoj1415】[Noi2005]聪聪和可可 期望记忆化搜索

    题目描述 输入 数据的第1行为两个整数N和E,以空格分隔,分别表示森林中的景点数和连接相邻景点的路的条数. 第2行包含两个整数C和M,以空格分隔,分别表示初始时聪聪和可可所在的景点的编号. 接下来E行 ...

  6. Rust学习资源和路线

    Rust学习资源和路线 来源 https://rust-lang-cn.org/article/23 学习资源 The Rust Programming Language 堪称Rust的"T ...

  7. 【POJ3693】Maximum repetition substring (SA)

    这是一道神奇的题目..论文里面说得不清楚,其实是这样...如果一个长度为l的串重复多次,那么至少s[1],s[l+1],s[2*l+1],..之中有相邻2个相等...设这时为j=i*l+1,k=j+l ...

  8. manifest

    manifest是一种软件,属于AndroidManifest.xml文件,在简单的Android系统的应用中提出了重要的信息,它可以运行任何应用程序的代码. 每个安卓应用程序必须有一个Android ...

  9. HTML+CSS 滚动条样式自定义 - 适用于 div,iframe, html 等

    友言:这两天被滚动条整的无与伦比,在此做下总结: 首先自定义浏览器滚动条的实现原理:计算浏览器滚动条的高度,层级1的高度与滚动条的总高度是一样的,通过相似比例计算: 浏览器滚动条总高度 :滚动条高度 ...

  10. 配置 L3 agent

    上一节我们介绍了路由服务(Routing)的基本功能,今天教大家如何配置. Neutron 的路由服务是由 l3 agent 提供的. 除此之外,l3 agent 通过 iptables 提供 fir ...