我尝试用Golang编写了一个通过Unix域套接字进行通信的回显服务器

我想要一个可以在ssh-agent中保存密钥的程序,为了练习,我首先写了一个echo服务器。我选择使用golang的原因如下。

    • bashからではunix domain socketを直接扱えないようなので諦めた

 

    • OSX, Linuxで動いて欲しい

 

    クロスコンパイルが簡単(らしい)

因为昨天是我第一次写golang,所以非常欢迎大家提供意见,相反,我更希望能够得到各种指正。

创建UNIX域套接字

因为类似于ssh-agent的情况,我们会在启动时自动配置套接字文件,而不是从外部指定。在寻找类似于mktemp -d的功能时,我发现了ioutil.TempDir函数并进行了使用。另外,为了防止被非执行者访问,我们还修改了访问权限。

main() {
  tempDir, err := ioutil.TempDir("", "golang-sample-echo-server.")
  pid := strconv.Itoa(os.Getpid())
  socket := tempDir + "/server." + pid
  listener, err := net.Listen("unix", socket)
  if err != nil {
    log.Printf("error: %v\n", err)
    return
  }
  if err := os.Chmod(socket, 0700); err != nil {
    log.Printf("error: %v\n", err)
    return
  }
}

处理内容

    1. 使用ioutil.TempDir()创建临时目录

 

    1. 获取pid并在创建的临时目录中创建socket文件

 

    将socket文件的权限限制为仅所有者可访问

等待接收

我在主函数中定义了一个函数来接收在上面创建的监听器,并进行调用。
我的代码是参考了《在Go中使用Unix套接字》(堆栈溢出)的方法。

func server(listener net.Listener) {
  for {
    fd, err := listener.Accept()
    if err != nil {
      return
    }
    go process(fd)
  }
}
func process(fd net.Conn) {
  for {
    buf := make([]byte, 512)
    nr, err := fd.Read(buf)
    if err != nil {
      break
    }
    data := buf[0:nr]
    fmt.Printf("Recieved: %v", string(data));
    _, err = fd.Write(data)
    if err != nil {
      log.Printf("error: %v\n", err)
      break
    }
  }
  fd.Close()
}

处理内容

    1. listener.Accept()用于等待输入。

 

    1. 当有输入时,调用process()函数,同时读取到固定大小的缓冲区并进行回显。

 

    1. 当没有可读取的数据时,关闭fd并结束。

 

    1. 由于在goroutine中调用process()函数,因此不需要等待处理返回结果就可以再次通过listener.Accept()等待输入。

 

    如果listener被关闭,listener.Accept()将返回错误,从而退出for循环。

結束時的清理

因为在这个过程中会留下一些垃圾,所以我们会适时进行清理。

我参考了使用Golang编写的Web应用程序通过UNIX域套接字进行公开的代码。

func shutdown(listener net.Listener, tempDir string, close chan int) {
  c := make(chan os.Signal, 2)
  signal.Notify(c, os.Interrupt, syscall.SIGTERM)
  go func() {
    interrupt := 0
    for {
      s := <-c
      switch s {
      case os.Interrupt:
        if (interrupt == 0) {
          fmt.Println("Interrupt...")
          interrupt++
          continue
        }
      }
      break
    }
    if err := listener.Close(); err != nil {
      log.Printf("error: %v\n", err)
    }
    if err := os.Remove(tempDir); err != nil {
      log.Printf("error: %v\n", err)
    }
    close <- 1
  }()
}

进行的操作

    1. 创建一个用于等待信号的频道

 

    1. 在goroutine中调用处理函数以响应信号的发生,并将频道设置为等待通知状态

 

    1. 在信号发生时,goroutine会重新启动注册的处理过程

 

    1. 如果是Ctrl+C,则设置为第一次忽略

 

    1. 调用listener.Close()和删除目录

 

    在调用者给定的频道上通知退出

最后的主函数

可构建的代码已经被推送到github上。

func main() {
  log.SetFlags(log.Lshortfile)
  tempDir, err := ioutil.TempDir("", "golang-sample-echo-server.")
  pid := strconv.Itoa(os.Getpid())
  socket := tempDir + "/server." + pid
  listener, err := net.Listen("unix", socket)
  if err != nil {
    log.Printf("error: %v\n", err)
    return
  }
  if err := os.Chmod(socket, 0700); err != nil {
    log.Printf("error: %v\n", err)
    return
  }
  close := make(chan int)
  shutdown(listener, tempDir, close)
  fmt.Printf("GOLANG_SAMPLE_SOCK=%v;export GOLANG_SAMPLE_SOCK;\n", socket)
  fmt.Printf("GOLANG_SAMPLE_PID=%v;export GOLANG_SAMPLE_PID;\n", pid)
  server(listener)
  _ = <-close
}

处理内容

从创建Unix域套接字开始的继续。

    1. 创建一个用于传递给shutdown()函数的channel,调用shutdown函数

 

    1. 输出用于客户端连接的socket和pid

 

    1. 将创建的listener传递给server()函数并启动

 

    1. 等待shutdown()函数的结束

执行两次Ctrl+C将调用shutdown()函数中的listener.Close()
listener.Close()导致server()函数中的listener.Accept()返回错误
接收listener.Accept()的错误导致server()函数的循环终止
等待shutdown的channel结束

确认动作

$ golang/bin/golang-sample-echo-server
GOLANG_SAMPLE_SOCK=/var/folders/ld/92yggdj91l72jq205zr0l1540000gn/T/golang-sample-echo-server.660221887/server.61435;export GOLANG_SAMPLE_SOCK;
GOLANG_SAMPLE_PID=61435;export GOLANG_SAMPLE_PID;
Recieved: sample echo server test
$ echo 'sample echo server test' | nc -U /var/folders/ld/92yggdj91l72jq205zr0l1540000gn/T/golang-sample-echo-server.660221887/server.61435
sample echo server test

剩余任务

    • コマンドラインオプションの対応を追加

 

    • 自らdaemon化するように処理を追加

 

    自らを停止するオプション・処理の追加

给您的附赠品

将日志简化易懂

在出现错误时,很难确定日志来自哪一行,让我感到非常困扰。
通过添加以下行到main函数的第一行,把格式改为[文件名]:[行号]: [消息],真是太感激了。

log.SetFlags(log.Lshortfile)

追加于2016年10月12日

因为有一些同事的指出,我做出了相应的处理。

fd.Close() 的操作已被更改为 defer。

只是将末尾的fd.Close()移到了开头,并加上了defer。

func process(fd net.Conn) {
  defer fd.Close()
  for {
    buf := make([]byte, 512)
    nr, err := fd.Read(buf)
    if err != nil {
      break
    }
    data := buf[0:nr]
    fmt.Printf("Recieved: %v", string(data));
    _, err = fd.Write(data)
    if err != nil {
      log.Printf("error: %v\n", err)
      break
    }
  }
}

将server转换成结构体。

    サーバーがListenerの管理をする為に、main関数内でやっていたことの一部をサーバー側に移動しています。
package main

import (
  "net"
  "log"
  "os"
  "fmt"
)

type Server struct {
  listener net.Listener
}

func NewServer() *Server{
  s := new(Server)
  return s;
}

func (s *Server) Open(socket string) {
  listener, err := net.Listen("unix", socket)
  if err != nil {
    log.Printf("error: %v\n", err)
    return
  }
  s.listener = listener;
  if err := os.Chmod(socket, 0700); err != nil {
    log.Printf("error: %v\n", err)
    s.Close()
    return
  }
}

func (s *Server) Close() {
  if err := s.listener.Close(); err != nil {
    log.Printf("error: %v\n", err)
  }
}

func (s *Server) Start() {
  for {
    fd, err := s.listener.Accept()
    if err != nil {
      return
    }
    go s.Process(fd)
  }
}

func (s *Server) Process(fd net.Conn) {
  defer fd.Close()
  for {
    buf := make([]byte, 512)
    nr, err := fd.Read(buf)
    if err != nil {
      break
    }
    data := buf[0:nr]
    fmt.Printf("Recieved: %v", string(data));
    _, err = fd.Write(data)
    if err != nil {
      log.Printf("error: %v\n", err)
      break
    }
  }
}

package main

import (
  "os"
  "os/signal"
  "io/ioutil"
  "fmt"
  "syscall"
  "strconv"
  "log"
)

func main() {
  log.SetFlags(log.Lshortfile)
  tempDir, err := ioutil.TempDir("", "golang-sample-echo-server.")
  if err != nil {
    log.Printf("error: %v\n", err)
    return
  }
  pid := strconv.Itoa(os.Getpid())
  socket := tempDir + "/server." + pid
  if err := os.Chmod(tempDir, 0700); err != nil {
    log.Printf("error: %v\n", err)
    return
  }
  defer func() {
    if err := os.Remove(tempDir); err != nil {
      log.Printf("error: %v\n", err)
    }
  }()
  server := NewServer()
  server.Open(socket)
  registerShutdown(server)
  fmt.Printf("GOLANG_SAMPLE_SOCK=%v;export GOLANG_SAMPLE_SOCK;\n", socket)
  fmt.Printf("GOLANG_SAMPLE_PID=%v;export GOLANG_SAMPLE_PID;\n", pid)
  server.Start()
}

func registerShutdown(server *Server) {
  c := make(chan os.Signal, 2)
  signal.Notify(c, os.Interrupt, syscall.SIGTERM)
  go func() {
    interrupt := 0
    for {
      s := <-c
      switch s {
      case os.Interrupt:
        if (interrupt == 0) {
          fmt.Println("Interrupt...")
          interrupt++
          continue
        }
      }
      break
    }
    server.Close()
  }()
}

追加说明于2016年10月13日

由于没有运动,所以在2016/10/14进行了微调。

因为还提到了错误处理,所以已经进行了相应的处理。

因为只有差异更容易理解,所以只提供差异。

服务器在出现错误时返回错误,实际的错误处理在主函数内进行。

diff --git a/main.go b/main.go
index 6478944..92dbdbf 100644
--- a/main.go
+++ b/main.go
@@ -29,11 +29,14 @@ func main() {
     }
   }()
   server := NewServer()
-  server.Open(socket)
+  if err := server.Open(socket); err != nil {
+    log.Printf("error: %v\n", err)
+    return;
+  }
   registerShutdown(server)
   fmt.Printf("GOLANG_SAMPLE_SOCK=%v;export GOLANG_SAMPLE_SOCK;\n", socket)
   fmt.Printf("GOLANG_SAMPLE_PID=%v;export GOLANG_SAMPLE_PID;\n", pid)
-  server.Start()
+  server.Start();
 }

 func registerShutdown(server *Server) {
@@ -53,6 +56,8 @@ func registerShutdown(server *Server) {
       }
       break
     }
-    server.Close()
+    if err := server.Close(); err != nil {
+      log.Printf("error: %v\n", err)
+    }
   }()
 }
diff --git a/server.go b/server.go
index 64a03af..3f40115 100644
--- a/server.go
+++ b/server.go
@@ -2,7 +2,6 @@ package main

 import (
   "net"
-  "log"
   "os"
   "fmt"
 )
@@ -16,37 +15,37 @@ func NewServer() *Server{
   return s;
 }

-func (s *Server) Open(socket string) {
+func (s *Server) Open(socket string) error {
   listener, err := net.Listen("unix", socket)
   if err != nil {
-    log.Printf("error: %v\n", err)
-    return
+    return err
   }
   s.listener = listener;
   if err := os.Chmod(socket, 0600); err != nil {
-    log.Printf("error: %v\n", err)
     s.Close()
-    return
+    return err
   }
+  return nil
 }

-func (s *Server) Close() {
+func (s *Server) Close() error{
   if err := s.listener.Close(); err != nil {
-    log.Printf("error: %v\n", err)
+    return err;
   }
+  return nil
 }

 func (s *Server) Start() {
   for {
     fd, err := s.listener.Accept()
     if err != nil {
-      return
+      break;
     }
     go s.Process(fd)
   }
 }

-func (s *Server) Process(fd net.Conn) {
+func (s *Server) Process(fd net.Conn) error{
   defer fd.Close()
   for {
     buf := make([]byte, 512)
@@ -58,8 +57,8 @@ func (s *Server) Process(fd net.Conn) {
     fmt.Printf("Recieved: %v", string(data));
     _, err = fd.Write(data)
     if err != nil {
-      log.Printf("error: %v\n", err)
-      break
+      return err
     }
   }
+  return nil

我使用了参考的网站

Go语言编写的Web应用程序可以通过UNIX域套接字进行公开。
在Go中使用UNIX套接字(来自Stack Overflow)。

使用原生的汉语对以下进行改写,只需要一个选项:
The Go编程语言
– net 包
使用Go编写的Web应用程序可以通过UNIX域套接字进行公开。
在Go中使用UNIX套接字(来自Stack Overflow)。

bannerAds