Сетевое программирование TCP/IP на языке Go
На первый взгляд соединение двух процессов через уровень TCP/IP может показаться пугающим, но в Go это проще, чем вы думаете.
Сценарии приложений для отправки данных на уровне TCP/IP
Конечно, во многих случаях, но не в большинстве случаев, несомненно, лучше использовать сетевой протокол более высокого уровня из-за доступных великолепных API, которые скрывают множество технических деталей. Теперь в соответствии с различными потребностями существует множество вариантов, таких как протокол очереди сообщений, gRPC, protobuf, FlatBuffers, API веб-сайта RESTful, веб-сокет и так далее.
Однако в некоторых особых случаях, особенно в небольших проектах, выбор любого другого метода будет казаться слишком раздутым, не говоря уже о необходимости введения дополнительных зависимостей.
К счастью, создать простую сетевую коммуникацию с помощью пакета net стандартной библиотеки не сложнее, чем вы видели.
Потому что язык Go имеет следующие два упрощения.
Упрощение 1: Соединение — это поток ввода-вывода
Интерфейс net.Conn реализует интерфейсы io.Reader, io.Writer и io.Closer. Таким образом, TCP-соединения можно рассматривать как любой другой поток ввода-вывода.
Вы можете подумать: «Хорошо, я могу отправлять строки или байтовые фрагменты в TCP, и это здорово, но как насчет сложных структур данных? Например, мы имеем дело с данными структурного типа?»
Упрощение 2: Go знает, как эффективно декодировать сложные типы
Когда дело доходит до отправки закодированных структурированных данных по сети, первое, что приходит на ум, — это JSON. Но подождите секунду — пакет кодирования/gob стандартной библиотеки Go предоставляет способ сериализации и генерации типов данных Go без добавления строковых тегов в структуры, несовместимого с Go JSON или ожидания. Используйте json.Unmarshal для кропотливого анализа текста в двоичные данные.
Кодирование и декодирование Gob может напрямую манипулировать потоком ввода-вывода, что идеально соответствует первому упрощению.
Далее мы реализуем простое приложение с помощью этих двух упрощенных правил.
Цель этого простого приложения
Приложение должно делать две вещи:
- Отправляйте и получайте простые строковые сообщения.
- Отправлять и получать структуры через gob.
Первая часть, «Отправка простых строк», покажет, насколько просто отправлять данные по сети TCP/IP, не прибегая к протоколам более высокого уровня. Вторая часть, зайдя немного глубже, отправляет полные структуры по сети, используя строки, слайсы, карты и даже рекурсивные указатели на самих себя.
Благодаря пакету gob сделать это не составляет труда.
客户端 服务端
待发送结构体 解码后结构体
testStruct结构体 testStruct结构体
| ^
V |
gob编码 ----------------------------> gob解码
| ^
V |
发送 ============网络================= 接收
Основные элементы отправки строковых данных по TCP
на отправителе
Для отправки строки требуется три простых шага:
- Откройте соединение, соответствующее принимающему процессу.
- написать строку.
- Закройте соединение.
Пакет net предоставляет пару методов для реализации этой функциональности.
- ResolveTCPDadr(): эта функция возвращает адрес конечной точки TCP.
- DialTCP(): Dial-подобный для сетей TCP.
Оба эти метода определены в файле src/net/tcpsock.go исходного кода go.
func ResolveTCPAddr(network, address string) (*TCPAddr, error) {
switch network {
case "tcp", "tcp4", "tcp6":
case "": // a hint wildcard for Go 1.0 undocumented behavior
network = "tcp"
default:
return nil, UnknownNetworkError(network)
}
addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address)
if err != nil {
return nil, err
}
return addrs.forResolve(network, address).(*TCPAddr), nil
}
ResolveTCPDadr() принимает два строковых параметра.
- network: должно быть имя сети TCP, например, tcp, tcp4, tcp6.
- address: строка адреса TCP. Если это не буквальный IP-адрес или номер порта не является буквальным номером порта, ResolveTCPDadr преобразует входящий адрес в адрес конечной точки TCP. В противном случае передайте пару буквального IP-адреса и номера порта в качестве адреса. Параметр адреса может использовать имя хоста, но это не рекомендуется, поскольку будет возвращено не более одного IP-адреса с именем хоста.
ResolveTCPDadr() получает строку, представляющую адрес TCP (например, localhost:80, 127.0.0.1:80 или [::1]:80, которые все представляют порт 80 локальной машины), возвращает (указатель net.TCPDadr , nil ) (возвращает (nil, error), если строка не может быть преобразована в действительный TCP-адрес).
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) {
switch network {
case "tcp", "tcp4", "tcp6":
default:
return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: raddr.opAddr(), Err: UnknownNetworkError(network)}
}
if raddr == nil {
return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: nil, Err: errMissingAddress}
}
c, err := dialTCP(context.Background(), network, laddr, raddr)
if err != nil {
return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: raddr.opAddr(), Err: err}
}
return c, nil
}
Функция DialTCP() принимает три параметра:
- network: этот параметр аналогичен сетевому параметру ResolveTCPDadr и должен быть сетевым именем TCP.
- laddr: Указатель типа TCPAddr, представляющий локальный адрес TCP.
- raddr: Указатель типа TCPAddr, представляющий удаленный TCP-адрес.
Он подключится для набора двух TCP-адресов и вернет это соединение в виде объекта net.TCPConn (сбой соединения возвращает ошибку). Если нам не нужен слишком большой контроль над настройками Dial, то вместо этого мы можем использовать Dial().
func Dial(network, address string) (Conn, error) {
var d Dialer
return d.Dial(network, address)
}
Функция Dial() принимает TCP-адрес и возвращает общий net.Conn. Этого достаточно для нашего теста. Однако, если вам нужна функциональность, доступная только для соединений TCP, вы можете использовать варианты TCP (DialTCP, TCPConn, TCPAddr и т. д.).
После успешного набора мы можем обрабатывать новое соединение так же, как и другие входные и выходные потоки, как описано выше. Мы даже можем обернуть соединение в bufio.ReadWriter, что позволяет использовать различные методы ReadWriter, такие как ReadString(), ReadBytes, WriteString и т. д.
func Open(addr string) (*bufio.ReadWriter, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, errors.Wrap(err, "Dialing "+addr+" failed")
}
// 将net.Conn对象包装到bufio.ReadWriter中
return bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), nil
}
Помните, что буферизованному Writer необходимо вызвать метод Flush() после записи, чтобы все данные были сброшены в базовое сетевое соединение.
Наконец, каждый объект подключения имеет метод Close() для завершения связи.
тонкая настройка
Структура номеронабирателя определяется следующим образом:
type Dialer struct {
Timeout time.Duration
Deadline time.Time
LocalAddr Addr
DualStack bool
FallbackDelay time.Duration
KeepAlive time.Duration
Resolver *Resolver
Cancel <-chan struct{}
}
- Тайм-аут: максимальное количество времени, в течение которого номеронабиратель ожидает завершения соединения. Если также установлен крайний срок, он может выйти из строя раньше. Тайм-аута по умолчанию нет. При использовании TCP и наборе имени хоста с несколькими IP-адресами тайм-аут делится между ними. С тайм-аутом или без него операционная система может установить более ранний тайм-аут. Например, время ожидания TCP обычно составляет около 3 минут.
- Крайний срок: это абсолютный момент времени, когда циферблат вот-вот выйдет из строя. Может выйти из строя раньше, если установлено время ожидания. Значение 0 означает, что крайний срок отсутствует либо в зависимости от операционной системы, либо в зависимости от параметра «Тайм-аут».
- LocalAddr: локальный адрес, используемый при наборе адреса. Этот адрес должен быть полностью совместимого типа с набираемым сетевым адресом. Если ноль, автоматически выбирается локальный адрес.
- DualStack: это свойство обеспечивает совместимость с RFC 6555.Счастливые глазные яблоки«Удаленный доступ, когда сеть является tcp, хост в параметре адреса может быть разрешен по адресам IPv4 и IPv6. Это позволяет клиенту допустить небольшой разрыв в сетевой спецификации семейства адресов.
- FallbackDelay: когда DualStack включен, указывает время ожидания перед созданием резервного соединения. Если установлено значение 0, время задержки по умолчанию составляет 300 мс.
- KeepAlive: указывает время поддержания активности для активных сетевых подключений. Если установлено значение 0, поддержка активности не включена. Сетевые протоколы, не поддерживающие поддержку активности, игнорируют это поле.
- Преобразователь: Необязательно, указывает альтернативный преобразователь для использования.
- Отмена: необязательный канал, закрытие которого указывает на то, что набор должен быть отменен. Не все типы набора поддерживают отмену набора. Устарело, вместо этого используйте DialContext.
Есть два варианта тонкой настройки.
Таким образом, интерфейс номеронабирателя предоставляет две возможности тонкой настройки:
- DeadLine и Timeout Options: настройки тайм-аута для неудачного набора номера.
- Опция KeepAlive: управление сроком службы соединения.
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
Интерфейс net.Conn представляет собой обычное сетевое соединение, ориентированное на поток. Он имеет следующие методы интерфейса:
- Read(): чтение данных из соединения.
- Write(): запись данных в соединение.
- Close(): закрыть соединение.
- LocalAddr(): возвращает локальный сетевой адрес.
- RemoteAddr(): возвращает удаленный сетевой адрес.
- SetDeadline(): устанавливает крайние сроки чтения и записи, связанные с соединением. Эквивалентно одновременному вызову SetReadDeadline() и SetWriteDeadline().
- SetReadDeadline(): устанавливает крайний срок ожидания для будущих вызовов чтения и заблокированных в настоящее время вызовов чтения.
- SetWriteDeadline(): устанавливает крайний срок ожидания для будущих вызовов записи, а также для текущих заблокированных вызовов записи.
Интерфейс Conn также имеет настройки крайнего срока, как для всего соединения (SetDeadLine()), так и для конкретных вызовов чтения и записи (SetReadDeadLine() и SetWriteDeadLine()).
Обратите внимание, что крайний срок является фиксированной точкой времени (настенные часы). В отличие от тайм-аута, они не сбрасываются после новой активности. Таким образом, каждое действие на соединении должно устанавливать новый крайний срок.
В приведенном ниже примере кода не используются крайние сроки, потому что он достаточно прост, чтобы мы могли легко увидеть, когда мы застряли. Когда Ctrl-C, мы вручную запускаем инструмент крайнего срока.
на принимающей стороне
Шаги на принимающей стороне следующие:
- Открытое прослушивание на локальном порту.
- Когда приходит запрос, создайте горутину для обработки запроса.
- В горутине прочитайте данные. При желании ответы также могут быть отправлены.
- Закройте соединение.
Для прослушивания необходимо указать номер порта для локального прослушивания. Вообще говоря, прослушивающее приложение (также называемое сервером) объявляет номер порта прослушивания.Если предоставляется стандартная служба, используется соответствующий порт, соответствующий этой службе. Например, веб-службы обычно прослушивают порт 80 для запросов HTTP и порт 443 для запросов HTTPS. Демон SSH по умолчанию прослушивает порт 22, а служба WHOIS использует порт 43.
type Listener interface {
// Accept waits for and returns the next connection to the listener.
Accept() (Conn, error)
// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
Close() error
// Addr returns the listener's network address.
Addr() Addr
}
func Listen(network, address string) (Listener, error) {
addrs, err := DefaultResolver.resolveAddrList(context.Background(), "listen", network, address, nil)
if err != nil {
return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: nil, Err: err}
}
var l Listener
switch la := addrs.first(isIPv4).(type) {
case *TCPAddr:
l, err = ListenTCP(network, la)
case *UnixAddr:
l, err = ListenUnix(network, la)
default:
return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: address}}
}
if err != nil {
return nil, err // l is non-nil interface containing nil pointer
}
return l, nil
}
Основная часть сетевого пакета для реализации сервера:
net.Listen() создает нового слушателя по заданному адресу локальной сети. Если ему передается только номер порта, например ":61000", то слушатель будет прослушивать все доступные сетевые интерфейсы. Это очень удобно, поскольку компьютеры обычно предоставляют по крайней мере два активных интерфейса, петлевой интерфейс и по крайней мере одну реальную сетевую карту. Эта функция возвращает Listener в случае успеха.
Интерфейс Listener имеет метод Accept() для ожидания поступления запроса. Затем он принимает запрос и возвращает вызывающему абоненту новое соединение. Accept() обычно вызывается в цикле и может обслуживать несколько соединений одновременно. Каждое соединение может обрабатываться отдельной горутиной, как показано в коде ниже.
раздел кода
Вместо того, чтобы код перемещал несколько байтов вперед и назад, я хотел бы, чтобы он продемонстрировал что-то более полезное. Я хочу, чтобы он мог отправлять на сервер разные команды с разными носителями данных. Сервер должен иметь возможность идентифицировать каждую команду и декодировать данные команды.
В нашем коде клиент отправляет два типа команд: «STRING» и «GOB». Все они заканчиваются новой строкой.
Команда "STRING" содержит строку строковых данных, которые можно обрабатывать простыми операциями чтения и записи в bufio.
Команда "GOB" состоит из структуры, которая содержит некоторые поля, включая слайс и карту, и даже указатель на себя. Как видите, при запуске этого кода неудивительно, что пакет gob может перемещать эти данные по нашему сетевому соединению (возня).
То, что мы имеем здесь, — это, по сути, некий специальный протокол (специальный, для конкретной цели, специальный, специальный), которому следуют и клиент, и сервер, с новой строкой после командной строки, а затем с данными. Для каждой команды сервер должен знать точный формат данных и способ их обработки.
Для этого серверный код использует двухэтапный подход.
- Шаг 1: Когда функция Listen() получает новое соединение, она порождает новую горутину для вызова handleMessage(). Эта функция считывает имя команды из соединения, запрашивает карту для соответствующей функции обработчика и вызывает ее.
- Шаг 2: Выбранная функция-обработчик считывает и обрабатывает данные из командной строки.
package main
import (
"bufio"
"encoding/gob"
"flag"
"github.com/pkg/errors"
"io"
"log"
"net"
"strconv"
"strings"
"sync"
)
type complexData struct {
N int
S string
M map[string]int
P []byte
C *complexData
}
const (
Port = ":61000"
)
Исходящие соединения
Использование запускающего соединения — это моментальный снимок. net.Conn удовлетворяет требованиям интерфейсов io.Reader и io.Writer, поэтому мы можем обрабатывать TCP-соединения, как и любые другие устройства чтения и записи.
func Open(addr string) (*bufio.ReadWriter, error) {
log.Println("Dial " + addr)
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, errors.Wrap(err, "Dialing " + addr + " failed")
}
return bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), nil
}
Откройте соединение с TCP-адресом. Он возвращает TCP-соединение с тайм-аутом и заключает его в буферизованный ReadWriter. Наберите удаленный процесс. Обратите внимание, что локальные порты выделяются на лету. Если вам необходимо указать номер локального порта, используйте метод DialTCP().
введите соединение
Этот раздел немного занимается подготовкой входящих данных. Согласно специальному протоколу, который мы представили ранее, имя команды + новая строка + данные + новая строка. Натуральные данные связаны с конкретными командами. Чтобы справиться с такой ситуацией, мы создаем объект Endpoint со следующими свойствами:
- Это позволяет зарегистрировать одну или несколько функций-обработчиков, каждая из которых может обрабатывать специальную команду.
- Он отправляет определенные команды соответствующей функции-обработчику на основе имени команды.
Во-первых, мы объявляем тип HandleFunc, который является типом функции, которая получает значение указателя bufio.ReadWriter, который является функцией-обработчиком, которую мы хотим зарегистрировать для каждой отдельной команды позже. Параметр, который он получает, представляет собой соединение net.Conn, обернутое интерфейсом ReadWriter.
type HandleFunc func(*bufio.ReadWriter)
Затем мы объявляем тип структуры Endpoint, который имеет три свойства:
- listener: объект Listener, возвращаемый функцией net.Listen().
- обработчик: карта для хранения зарегистрированных функций обработчика.
- m: мьютекс, используемый для решения проблемы небезопасности карты с несколькими горутинами.
type Endpoint struct {
listener net.Listener
handler map[string]HandleFunc
m sync.RWMutex // Maps不是线程安全的,因此需要互斥锁来控制访问。
}
func NewEndpoint() *Endpoint {
return &Endpoint{
handler: map[string]HandleFunc{},
}
}
func (e *Endpoint) AddHandleFunc(name string, f HandleFunc) {
e.m.Lock()
e.handler[name] = f
e.m.Unlock()
}
func (e *Endpoint) Listen() error {
var err error
e.listener, err = net.Listen("tcp", Port)
if err != nil {
return errors.Wrap(err, "Unable to listen on "+e.listener.Addr().String()+"\n")
}
log.Println("Listen on", e.listener.Addr().String())
for {
log.Println("Accept a connection request.")
conn, err := e.listener.Accept()
if err != nil {
log.Println("Failed accepting a connection request:", err)
continue
}
log.Println("Handle incoming messages.")
go e.handleMessages(conn)
}
}
// handleMessages读取连接到第一个换行符。 基于这个字符串,它会调用恰当的HandleFunc。
func (e *Endpoint) handleMessages(conn net.Conn) {
// 将连接包装到缓冲reader以便于读取
rw := bufio.NewReadWrite(bufio.NewReader(conn), bufio.NewWriter(conn))
defer conn.Close()
// 从连接读取直到遇到EOF. 期望下一次输入是命令名。调用注册的用于该命令的处理器。
for {
log.Print("Receive command '")
cmd, err := rw.ReadString('\n')
switch {
case err == io.EOF:
log.Println("Reached EOF - close this connection.\n ---")
return
case err != nil:
log.Println("\nError reading command. Got: '" + cmd + "'\n", err)
}
// 修剪请求字符串中的多余回车和空格- ReadString不会去掉任何换行。
cmd = strings.Trim(cmd, "\n ")
log.Println(cmd + "'")
// 从handler映射中获取恰当的处理器函数, 并调用它。
e.m.Lock()
handleCommand, ok := e.handler[cmd]
e.m.Unlock()
if !ok {
log.Println("Command '" + cmd + "' is not registered.")
return
}
handleCommand(rw)
}
}
Функция NewEndpoint() является фабричной функцией Endpoint. Он только инициализирует карту обработчика. Для упрощения предположим, что порт, который слушает наш терминал, фиксирован.
Тип Endpoint объявляет несколько методов:
- AddHandleFunc(): безопасно добавляет функцию обработчика к свойству обработчика, которая обрабатывает определенный тип команды с использованием мьютекса.
- Listen(): начать прослушивание на всех интерфейсах терминального порта. Перед вызовом Listen должна быть зарегистрирована хотя бы одна функция-обработчик через AddHandleFunc().
- HandleMessages(): Оберните соединение с помощью bufio, затем прочитайте его в два этапа, сначала прочитайте команду и добавьте новую строку, мы получим имя команды. Затем функция-обработчик, соответствующая зарегистрированной команде, получается через обработчик, а затем функция отправляется для выполнения чтения и анализа данных.
Обратите внимание, как динамические функции используются выше. Найдите конкретную функцию по названию команды, а затем присвойте конкретную функцию handleCommand.На самом деле тип переменной — это тип HandleFunc, то есть ранее объявленный тип функции-обработчика.
Перед вызовом метода Listen конечной точки необходимо зарегистрировать хотя бы одну функцию-обработчик. Итак, ниже мы определяем два типа функций-обработчиков: handleStrings и handleGob.
Функция handleStrings() получает и обрабатывает функции обработчика в нашем мгновенном протоколе, которые отправляют только строковые данные. Функция handleGob() представляет собой сложную структуру, которая получает и обрабатывает отправленные данные gob. handleGob немного сложнее, помимо чтения данных нам нужно их декодировать.
Мы видим, что мы используем rw.ReadString('n') дважды подряд для чтения строки, останавливаемся, когда встречается новая строка, и сохраняем прочитанное содержимое в строку. Обратите внимание, что эта строка содержит завершающую новую строку.
Кроме того, для обычных строковых данных мы напрямую используем bufio, чтобы обернуть подключенную строку ReadString для чтения. Для сложных структур gob мы используем gob для декодирования данных.
func handleStrings(rw *bufio.ReadWriter) {
log.Print("Receive STRING message:")
s, err := rw.ReadString('\n')
if err != nil {
log.Println("Cannot read from connection.\n", err)
}
s = strings.Trim(s, "\n ")
log.Println(s)
-, err = rw.WriteString("Thank you.\n")
if err != nil {
log.Println("Cannot write to connection.\n", err)
}
err = rw.Flush()
if err != nil {
log.Println("Flush failed.", err)
}
}
func handleGob(rw *bufio.ReadWriter) {
log.Print("Receive GOB data:")
var data complexData
dec := gob.NewDecoder(rw)
err := dec.Decode(&data)
if err != nil {
log.Println("Error decoding GOB data:", err)
return
}
log.Printf("Outer complexData struct: \n%#v\n", data)
log.Printf("Inner complexData struct: \n%#v\n", data.C)
}
Клиентские и серверные функции
Все готово, мы можем подготовить наши клиентские и серверные функции.
- Клиентская функция подключается к серверу и отправляет запросы STRING и GOB.
- Сервер начинает прослушивать запросы и запускает соответствующий обработчик.
// 当应用程序使用-connect=ip地址的时候被调用
func client(ip string) error {
testStruct := complexData{
N: 23,
S: "string data",
M: map[string]int{"one": 1, "two": 2, "three": 3},
P: []byte("abc"),
C: &complexData{
N: 256,
S: "Recursive structs? Piece of cake!",
M: Map[string]int{"01": "10": 2, "11": 3},
},
}
rw, err := Open(ip + Port)
if err != nil {
return errors.Wrap(err, "Client: Failed to open connection to " + ip + Port)
}
log.Println("Send the string request.")
n, err := rw.WriteString("STRING\n")
if err != nil {
return errors.Wrap(err, "Could not send the STRING request (" + strconv.Itoa(n) + " bytes written)")
}
// 发送STRING请求。发送请求名并发送数据。
log.Println("Send the string request.")
n, err = rw.WriteString("Additional data.\n")
if err != nil {
return errors.Wrap(err, "Could not send additional STRING data (" + strconv.Itoa(n) + " bytes written)")
}
log.Println("Flush the buffer.")
err = rw.Flush()
if err != nil {
return errors.Wrap(err, "Flush failed.")
}
// 读取响应
log.Println("Read the reply.")
response, err := rw.ReadString('\n')
if err != nil {
return errors.Wrap(err, "Client: Failed to read the reply: '" + response + "'")
}
log.Println("STRING request: got a response:", response)
// 发送GOB请求。 创建一个encoder直接将它转换为rw.Send的请求名。发送GOB
log.Println("Send a struct as GOB:")
log.Printf("Outer complexData struct: \n%#v\n", testStruct)
log.Printf("Inner complexData struct: \n%#v\n", testStruct.C)
enc := gob.NewDecoder(rw)
n, err = rw.WriteString("GOB\n")
if err != nil {
return errors.Wrap(err, "Could not write GOB data (" + strconv.Itoa(n) + " bytes written)")
}
err = enc.Encode(testStruct)
if err != nil {
return errors.Wrap(err, "Encode failed for struct: %#v", testStruct)
}
err = rw.Flush()
if err != nil {
return errors.Wrap(err, "Flush failed.")
}
return nil
}
Клиентская функция выполняется, когда при выполнении приложения указан флаг подключения, как вы можете видеть в коде ниже.
Ниже приведена серверная программа server. Сервер прослушивает входящие запросы и отправляет их зарегистрированным конкретным обработчикам в соответствии с именем команды запроса.
func server() error {
endpoint := NewEndpoint()
// 添加处理器函数
endpoint.AddHandleFunc("STRING", handleStrings)
endpoint.AddHandleFunc("GOB", handleGOB)
// 开始监听
return endpoint.Listen()
}
основная функция
Следующая основная функция может запускать либо клиент, либо сервер, в зависимости от того, установлен ли флаг соединения. Без этого флага сервер запускает процесс и прослушивает входящие запросы. Если есть флаг, запустите как клиент и подключитесь к хосту, указанному этим флагом.
Оба процесса можно запустить на одном компьютере, используя localhost или 127.0.0.1.
func main() {
connect := flag.String("connect", "", "IP address of process to join. If empty, go into the listen mode.")
flag.Parse()
// 如果设置了connect标志,进入客户端模式
if *connect != '' {
err := client(*connect)
if err != nil {
log.Println("Error:", errors.WithStack(err))
}
log.Println("Client done.")
return
}
// 否则进入服务端模式
err := server()
if err != nil {
log.Println("Error:", errors.WithStack(err))
}
log.Println("Server done.")
}
// 设置日志记录的字段标志
func init() {
log.SetFlags(log.Lshortfile)
}
Как получить и запустить код
Шаг 1: Получите код. Обратите внимание, что флаг -d автоматически устанавливает двоичные файлы в каталог $GOPATH/bin.
go get -d github.com/appliedgo/networking
Шаг 2: перейдите в каталог с исходным кодом. cd $GOPATH/src/github.com/appliedgo/networking
Шаг 3: Запустите сервер. запустить networking.go
Шаг 4: Откройте другую оболочку, также войдите в каталог исходного кода (шаг 2) и запустите клиент. go run networking.go -connect localhost
Tips
Если вы хотите немного изменить исходный код, вот несколько советов:
- Запустите клиент и сервер на разных машинах (в одной локальной сети).
- Добавьте в комплекс данных больше карт и указателей и посмотрите, как gob с этим справится (справится).
- Запустите несколько клиентов одновременно и посмотрите, сможет ли сервер их обработать.
09.02.2017: Карты не являются потокобезопасными, поэтому, если одна и та же карта используется в разных горутинах, для управления доступом к карте следует использовать мьютекс.
В приведенном выше коде карта добавляется до запуска горутины, поэтому вы можете безопасно изменить код для вызова AddHandleFunc(), когда горутина handleMessages уже запущена.
Краткое изложение знаний, полученных в этой главе
---- 2018-05-04 -----
- Применение буфио.
- гоб приложение.
- Карты небезопасно распределять между несколькими горутинами.