Создание своего Java Socket Server. Первый контакт

Итак, пришло время того самого момента, для чего мы все это затеяли — сделать соединение между клиентом и сервером.

В этой части мы создадим сервер при помощи библиотеки Netty, сделаем библиотеку на AS3 и проверим её. Чтобы понять процесс остановимся на простой реализации — клиент будет отправлять серверу сообщение, а сервер будет отправлять его обратно (это называется echo-сервер).

Делать мы это будем путем расширения кода, написанного в предыдущих частях.

Создаем java-socket сервер.

Создание сервера на Netty — процесс очень простой. Достаточно раз понять архитектуру этой библиотеки. Подробно с работой этой библиотеки можно познакомиться на сайте проекта.

Создаем новый канал.

Первым делом создаем фабрику для создания и управления Каналами и связанными ресурсами. Она будет обрабатывать все I/O запросы и генерировать ChannelEvent, которые мы уже будем обрабатывать

private static void initServer() {
	factory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool());
	initGeneralChannelHandler();
}

Дальше создаем и настраиваем сам канал:

private static void initGeneralChannelHandler() {
	// Configure the server.
	ServerBootstrap bootstrap = new ServerBootstrap(factory);
	generalChannelHandler = new GeneralChannelHandler();

	// Set up the pipeline factory.
	bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
		public ChannelPipeline getPipeline() {
			return Channels.pipeline(generalChannelHandler);
		}
	});

	//Socket options
	bootstrap.setOption("child.tcpNoDelay", true);
	bootstrap.setOption("child.keepAlive", true);

	// Bind and start to accept incoming connections.
	bootstrap.bind(new InetSocketAddress(properties.getInt("server.port")));
}

ServerBootstrap является вспомогательным классом для настройки сервера. Использовать его необязательно, но это удобный стандартный вариант для создания канала. Итак, мы создаем и настраиваем ChannelPipelineFactory: при каждом новом соединении будет вызываться getPipeline() — в нашем случае это наш созданный класс GeneralChannelHandler (о нем будет написано ниже). Также можно указать специфические параметры канала (bootstrap.setOption(«child.*», …)). И последним делом мы указываем порт по которому будет происходить соединение — этот параметр мы берем из файла condif.xml:

<entry key="server.port">9777</entry>

Теперь подробней об GeneralChannelHandler.

GeneralChannelHandler должен быть наследником SimpleChannelHandler. Этот класс является обработчиком практически всех событий, которые могут происходить с сокетами. В нем и происходит основная магия — это мост между логикой сервера и Netty. В нашем примере мы переопределим 2 события:

  1. Событие получения сообщения: messageReceived
    В нем мы будем отправлять в канал клиента то, что получили от него.
  2. Событие появления исключения: exceptionCaught
    Будем записывать с лог все ошибки, которые будут генерироваться сервером.

На самом деле это все делается элементарно:

public class GeneralChannelHandler extends SimpleChannelHandler {

	private static final Logger logger = Logger.getLogger(GeneralChannelHandler.class);

	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
		// Send back the received message to the remote peer.
		e.getChannel().write(e.getMessage());
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
		// Log the exception
		logger.error("Exception", e.getCause());
	}

}

Все — теперь у нас есть echo-сервер сервер. Можно подключиться по telnet и убедиться в его работе: любое передаваемое на сервер сообщение будет возвращаться обратно.

Создаем swc-библиотеку.

Для библиотеки я использовал следующую архитектуру, которая мне показалась уместной (JSS — Java Soket Server):
JSSApi — основной класс для работы с нашим java-сервером. Сейчас он просто создает экземпляр класса соединения и делегирует основную работу JSSConnection. Сейчас в этом нет никакого смысла но нейрон, отвечающий за программирование говорит, что должна быть разница между «Соединением» и «Api».

public class JSSApi extends EventDispatcher{

	private var _connection:JSSConnection;

	public function JSSApi(debug:Boolean = false) {
		_connection = new JSSConnection(this, debug);
	}

	/*---------------------------- Delegated methods ------------------------*/
	...

}

JSSConnection — класс, который будет заниматься низкоуровневой работой с сокетами. Код приведен не полностью: я обращу Ваше внимание на метод соединения, отправку и получения данных. Полные исходники можно будет скачать по ссылке в конце статьи.

public class JSSConnection {

	public function JSSConnection(dispatcher:IEventDispatcher, debug:Boolean = false) {
		_dispatcher = dispatcher;
		_debug = debug;
		createSocketAndAddHandlers();
	}

	...

	public function connect(host:String, port:int):void {
		_host = host;
		_port = port;

		try {
			log("Connect to " + _host + ":" + _port);
			_socket.connect(_host, _port);
		} catch (error:SecurityError) {
			log("Security error: " + error);
			_dispatcher.dispatchEvent(new JSSEvent(JSSEvent.SECURITY_ERROR, error.toString()));
		} catch (error:Error) {
			log("Connection error: " + error);
			throw error;
		}
	}

	...

	public function sendRequest(request:String):void {
		if (_socket != null && _socket.connected) {
			request += "\n";

			try {
				_socket.writeUTFBytes(request);
				_socket.flush();
				log("Message sent: " + request.toString());
			} catch(error:Error) {
				log("Error sending data: " + error);
			}

		} else {
			if (_isConnected) {
				dispatchConnectionLost(ClientDisconnectionReason.SOCKED_CONNECTED_FAIL);
			} else {
				throw new Error("Sending request without connection");
			}
		}
	}

	...

	private function handleSocketData(event:ProgressEvent):void {
		receiveData(_socket.readUTFBytes(_socket.bytesAvailable));
	}

	private function receiveData(msg:String):void {
		log("Message received: " + msg);
		_dispatcher.dispatchEvent(new JSSEvent(JSSEvent.RECEIVE_DATA, msg));
	}


	...

}

Компилируем swc библиотеку для работы с сервером. Теперь осталось убедиться, что наш сервер работает. Для этого создадим тестовый клиент и добавить его в доверенные (добавить в FlashPlayerTrust):

public class Flash_client extends Sprite {
	private var _jss:JSSApi;

	public function Flash_client() {

		_jss = new JSSApi(true);
		_jss.addEventListener(JSSEvent.CONNECT_TO_SERVER_SOCKET, handleConnect)
		_jss.addEventListener(JSSEvent.RECEIVE_DATA, handleReceiveData)

		trace("Connecting...");
		_jss.connect("localhost", 9777);

	}

	private function handleConnect(event:JSSEvent):void {
		trace("Connected");
		var messageToServer:String = "Hello!";
		trace("Sending Data: " + messageToServer);
		_jss.sendRequest(messageToServer);

	}

	private function handleReceiveData(event:JSSEvent):void {
		var messageFromServer:String = String(event.data);
		trace("Received: " + messageFromServer);
	}
}

В результате в консоли клиента мы увидим успешно отработанный сценарий (строки с временем — это служебный trace сгенерированный классом JSSConnection):

[trace] Connecting...
[trace] 23:28:08.691 Connect to localhost:9777
[trace] 23:28:08.865 Connected: [Event type="connect" bubbles=false cancelable=false eventPhase=2]
[trace] Connected
[trace] Sending Data: Hello!
[trace] 23:28:08.869 Message sent: Hello!
[trace] 23:28:08.874 Message received: Hello!
[trace] Received: Hello!

Это, конечно, ещё незавершенная версия даже для такой небольшой функциональности нужно сделать ряд дополнений. Но уже сейчас видна основная структура работы сервера. Если у Вас возникнут сложности — готов помочь и дополнить статью необходимыми данными.

Исходный код.

Полный код проекта всегда можно будет взять из репозитория на Google code. Коммит с приведенной выше версией находится тут.

2011.09.10