Go Language. Небольшое клиент-серверное приложение

Этот код написан с целью самообучения. Чтоб закрепить материал я решил немного прокомментировать проделанную работу.
Сразу скажу: на компилируемых языках не писал.

Что делает приложение


[к] — клиент
[c] — сервер
1. По установленному TCP соединению, [к] передает публичный ключ rsa.
2. При помощи принятого публичного ключа, [c] шифрует и отправляет сообщения [к]
3. [к] расшифровывает и выводит сообщения.

Вот как это выглядит в консоли:
image


Сервер


package main

Импортируем нужные пакеты
import(
	// Пакет для ввода/вывода данных. В данном случае из консоли
	"fmt"
			
	// Пакет для передачи информации по Unix networks sockets, including TCP/IP, UDP протоколам.
	// В данном случае будем использовать TCP протокол.
	"net"
		
	// Пакет для кроссплатформенной взаимодействия с операционной системой
	"os"
	
	// Реализует буфер ввода/вывода
	"bufio"
	
	// При помощи этого пакета будем шифровать и дешифровать передаваемую информацию
	"crypto/rsa"
	
	// Пакет для кроссплатформенной генерации случайных чисел
	"crypto/rand"
	
	// Для создания хешей методом sha1
	"crypto/sha1"
	
	// Для конвертации строковых данных в основные типы данных и обратно
	"strconv"
	
	// Для работы с большими числами
	"big"
)
Дальнейшее обращение к пакету будет происходить по его имени через точку.
К примеру: fmt.Println(), os.Exit() и т.д.
Если вы не уверены в том какое имя пакета вам нужно то можно посмотреть на исходники пакета на самую верхнюю сточку кода.
К примеру для crypto/rsa Шестая строчка rsa.go
При помощи команды goinstall вы сможете установить пакеты от других разработчиков.
В этом случае вы будите импортировать что-то типа этого «bitbucket.org/user/project», «github.com/user/project» или «project.googlecode.com/hg»

Объявим нужные нам константы
const(
	// Используемый tcp протокол
	tcpProtocol = "tcp4"
	
	// Длина генерируемого rsa ключа
	keySize = 1024
	
	// Максимальная длина шифруемого сообщения в байтах
	readWriterSize = keySize/8
)


Для того чтоб держать вместе соединение «с» и ключ от этого соединения «pubK» объявим тип данных remoteConn как структуру:
type remoteConn struct {
	c *net.TCPConn
	pubK *rsa.PublicKey	
}
Звёздочка "*", перед типом переменной, означает что переменная является ссылкой на данные объявленного типа
net.TCPConn — тип данных, который содержит структуру информации о TCP соединении.
rsa.PublicKey — тип данных. Нужен для зашифровки передаваемых сообщений.

В целях ознакомления будем обрабатывать возникающие ошибки таким образом:
Функция принимает одно значение err у которого тип os.Error.
В данном случае мы работаем с типом Error из пакета os (os.Error).
func checkErr(err os.Error){ 
	if err != nil {
		// Выводим текст ошибки
		fmt.Println(err) 

		// Завершаем программу
		os.Exit(1) 
	}
}


Объявим глобальную переменную listenAddr которая будет ссылкой на структуру типа net.TCPAddr
var listenAddr = &net.TCPAddr{IP: net.IPv4(192,168,0,4), Port: 0}
Амперсанд "&" перед net.TCPAddr вернёт ссылку на этот тип.
«Port: 0» в данном случае означает — любой свободный порт.

Следующая функция объединяет, соединение и публичный ключ для шифрования этого соединения в структуру remoteConn.
Причём возвращает ссылку на remoteConn а не значение.
func getRemoteConn(c *net.TCPConn) *remoteConn{
	return &remoteConn{c: c, pubK: waitPubKey(bufio.NewReader(с))}
}
bufio.NewReader(с) — создает буфер байт от соединения «c». Тип возвращаемых данных *bufio.Reader (ссылка на bufio.Reader)
waitPubKey() — ожидает от «клиента» когда тот в определённой последовательности передаст PublicKey

Функция принимает ссылку на буфер (*bufio.Reader) который в свою очередь содержит все байты пришедшие от соединение «c»
// Вернёт ссылку на структуру данных rsa.PublicKey
func waitPubKey(buf *bufio.Reader) (*rsa.PublicKey) {
	
	// Читаем строку из буфера
	line, _, err := buf.ReadLine(); checkErr(err)
	
	// Так как тип line - []byte (срез байт)
	// то для удобства сравнения переконвертируем <code><b>line</b></code> в строку
	if string(line) == "CONNECT" {
		
		// Далее мы будем читать буфер в том же порядке, в котором отправляем данные с клиента
		line, _, err := buf.ReadLine(); checkErr(err) // Читаем PublicKey.N

		// Создаём пустой rsa.PublicKey
		pubKey := rsa.PublicKey{N: big.NewInt(0)} 
		// pubKey.N == 0 
		// тип pubKey.N big.Int http://golang.org/pkg/big/#Int
		
		// Конвертируем полученную строку и запихиваем в pubKey.N big.Int
		pubKey.N.SetString(string(line), 10)
		// Метод SetString() получает 2 параметра:
		// string(line) - конвертирует полученные байты в строку
		// 10 - система исчисления используемая в данной строке 
		// (2 двоичная, 8 восьмеричная, 10 десятичная, 16 шестнадцатеричная ...)
		
		// Читаем из буфера второе число для pubKey.E
		line, _, err = buf.ReadLine(); checkErr(err)

		// Используемый пакет strconv для конвертации тип string в тип int
		pubKey.E, err = strconv.Atoi(string(line)); checkErr(err)
		
		// возвращаем ссылку на rsa.PublicKey
		return &pubKey
		
	} else {
		
		// В этом случае дальнейшее действия программы не предусмотренною. По этому:
		// Выводим что получили
		fmt.Println("Error: unkown command ", string(line)) 
		os.Exit(1) // Завершаем программу
	}
	return nil
}


Следующая функция является методом для ссылки на переменную типа remoteConn
Проделывает ряд действий для зашифровки и отправки сообщения
func (rConn *remoteConn) sendCommand(comm string) {
	
	// Зашифровываем сообщение
	eComm, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, rConn.pubK, []byte(comm), nil)
	// sha1.New() вернёт данные типа hash.Hash
	// С таким же успехм можно использовать sha512.New() sha256.New() ...
	// rand.Reader тип которого io.Reader позволяет не задумываясь о платформе генерировать
	// случайные числа из /dev/unrandom будь то Linux или CryptGenRandom API будь то Windows
	// rConn.pubK - публичный ключ который мы получили в func waitPubKey
	// []byte(comm) - конвертируем строку comm в срез байт ([]byte)
	checkErr(err) // проверяем на ошибки
	
	// Передаём зашифрованное сообщение по заранее установленному соединению
	rConn.c.Write(eComm)
	// rConn.c какого типа? - net.TCPConn у которого есть метод Write() 
	// http://golang.org/pkg/net/#TCPConn.Write
}


Ниже функция которая оперирует ранее объявленными функциями и в конечном итоге отправляет «клиенту» название сервера и приветствия на разных языках.
func listen() {
	// Слушаем любой свободны порт
	l, err := net.ListenTCP(tcpProtocol, listenAddr); checkErr(err)
	
	// Выведем прослушиваемый порт
	fmt.Println("Listen port: ", l.Addr().(*net.TCPAddr).Port)
	// l == *net.TCPListener == ссылка на тип данных
	// .Addr() http://golang.org/pkg/net/#TCPListener.Addr == метод для *net.TCPListener который возвращает "интерфейс"
	// net.Addr http://golang.org/pkg/net/#Addr который в свою очередь содержит ссылку на TCPAddr - *net.TCPAddr 
	// и два метода Network() и String()

	c, err := l.AcceptTCP(); checkErr(err)
	// На этом этапе программа приостанавливает свою работу ожидая соединения по прослушиваемому порту
	// AcceptTCP() - метод для *net.TCPListener http://golang.org/pkg/net/#TCPListener.AcceptTCP 
	//Возвращает установленное соединение и ошибку
	
	fmt.Println("Connect from:", c.RemoteAddr())
	// Вот 3 варианта которые подставив в fmt.Print[f|ln]() получим одинаковый результат
	// 1. c.RemoteAddr()
	// 2. c.RemoteAddr().(*net.TCPAddr)
	// 3. c.RemoteAddr().String()
	// В первый двух случаях функции: fmt.Println(), fmt.Print(), fmt.Printf() попытаются найти метод String()
	// Иначе вывод будет таким как есть
	
	// Таким образом мы получим соединение и ключ которым можно зашифровать это соединение
	rConn := getRemoteConn(с)

	// Шифруем и отправляем сообщения
	rConn.sendCommand("Go Language Server v0.1 for learning")
	rConn.sendCommand("Привет!")
	rConn.sendCommand("Привіт!")
	rConn.sendCommand("Прывітанне!")
	rConn.sendCommand("Hello!")
	rConn.sendCommand("Salut!")
	rConn.sendCommand("ハイ!")
	rConn.sendCommand("您好!")
	rConn.sendCommand("안녕!")
	rConn.sendCommand("Hej!")
}


На этом рассмотрение сервера окончено
func main() {
	listen()
}


Клиент


package main

import(
	"fmt"
	"net"
	"os"
	"bufio"
	"crypto/rsa"
	"crypto/rand"
	"crypto/sha1"
	"strconv"
)

const(
	tcpProtocol	= "tcp4"
	keySize = 1024
	readWriterSize = keySize/8
)

func checkErr(err os.Error){
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

var connectAddr = &net.TCPAddr{IP: net.IPv4(192,168,0,2), Port: 0}


// Считываем с командной строки нужный нам порт и пытаемся соединится с сервером
func connectTo() *net.TCPConn{
	// Выводим текст "Enter port:" без перехода но новую строку
	fmt.Print("Enter port:")
	
	// Считываем число с консоли в десятичном формате "%d"
	fmt.Scanf("%d", &connectAddr.Port)
	// Scanf не возвращает значение зато замечательно работает если передать туда ссылку
	
	fmt.Println("Connect to", connectAddr)
	
	// Создаём соединение с сервером
	c ,err := net.DialTCP(tcpProtocol, nil, connectAddr); checkErr(err)
	return c
}

// Функция в определённом порядке отправляет PublicKey
func sendKey(c *net.TCPConn, k *rsa.PrivateKey) {
	
	// Говорим серверу что сейчас будет передан PublicKey
	c.Write([]byte("CONNECT\n"))
	
	// передаём N типа *big.Int
	c.Write([]byte(k.PublicKey.N.String() + "\n"))
	// String() конвертирует *big.Int в string
	
	// передаём E типа int
	c.Write([]byte(strconv.Itoa(k.PublicKey.E) + "\n"))
	// strconv.Itoa() конвертирует int в string
	
	// []byte() конвертирует "строку" в срез байт
}


// Читает и освобождает определённый кусок буфера
// Вернёт срез байт
func getBytes(buf *bufio.Reader, n int) []byte {
	// Читаем n байт
	bytes, err:= buf.Peek(n); checkErr(err)
	// Освобождаем n байт
	skipBytes(buf, n)
	return bytes
}

// Освобождает, пропускает определённое количество байт
func skipBytes(buf *bufio.Reader, skipCount int){
	for i:=0; i<skipCount; i++ {
		buf.ReadByte()
	}
}

func main() {
	// Соединяемся с сервером
	c := connectTo()
	
	// Буферизирует всё что приходит от соединения "c"
	buf := bufio.NewReader(с)
	
	// Создаём приватный ключ в составе которого уже есть публичный ключ 
	k, err := rsa.GenerateKey(rand.Reader, keySize); checkErr(err)
	
	// Отправляем серверу публичный ключ
	sendKey(c, k)

	// В цикле принимаем зашифрованные сообщения от сервера
	for {
		// Получаем зашифрованное сообщение в байтах
		cryptMsg := getBytes(buf, readWriterSize)
		
		// Расшифровываем сообщение
		msg, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, k, cryptMsg, nil)
		
		// Проверяем на ошибку
		checkErr(err)
		
		// Выводим расшифрованное сообщение
		fmt.Println(string(msg))
	}
}


Исходники без единого комментария можно найти тут:
code.google.com/p/learning-go-language/source/browse
Поделиться публикацией

Похожие публикации

Комментарии 13

    –9
    Обоги! Для чего такие капитанские комментарии в коде?!
      +14
      А я думаю, что лучше такие, чем никаких.
        +7
        Большинство комментариев повторяют названия функций, что тут хорошего?
          –1
          Одобряю, плохих комментариев не бывает(если только они не противоречат коду).
            +3
            Бывает. Комментарии по идее нужны только там где непонятный с первого взгляда код. Да и комментировать нужно не то что делается, а почему делается именно так.
              +1
              Признаюсь, я умерено прокомментировал практически каждую строчку для того, чтобы каждый смог разобраться в этом коде.
              Для людей, которым комментарии не приносят пользы, в конце статьи есть ссылка.
              Перенести в шапку?
                0
                Я не это имел ввиду, в статье для новичков это уместно… Другое дело, что в рабочих проектах такого энтузиазма быть не должно :-) Спасибо за хорошую статью.
        0
        интересный язык, а он кроме этого гденить используется?
        0
        //Шифруем и отправляем сообщения, насрав на то, что хочет от нас клиент
        rConn.sendCommand(«Go Language Server v0.1 for learning»)
        rConn.sendCommand(«Привет!»)
        rConn.sendCommand(«Привіт!»)
        rConn.sendCommand(«Прывітанне!»)
        rConn.sendCommand(«Hello!»)
        rConn.sendCommand(«Salut!»)
        rConn.sendCommand("ハイ!")
        rConn.sendCommand("您好!")
        rConn.sendCommand("안녕!")
        rConn.sendCommand(«Hej!»)
          0
          Чудесно…
          Но зачем?
            0
            16 августа 2011 в 23:41
            конечно, чтобы попробовать язык и кросс компиляцию

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое