最新消息:

构建一个robust的java 服务器(部分翻译+附上自己写的客户端)

JAVA 大步 829浏览 0评论
用java构建一个java server很容易,但是要构建一个robust java server就很复杂。
一般的,我们可以使用一个ServerSocket来构建一个极其简单的server。
1.创建ServerSocket类的实例,提供一个监听的端口和一个backlog 连接数以允许在拒绝连接之前对其挂起。
2.调用ServerSocket类的accept()方法,等待来自客户端的连接
3.accept方法返回一个Socket,然后我们就可以使用这个Socket对象与客户端进行通信了
但是这种方式只能同时处理一个请求。所以,我们需要构建一个能同时处理多个请求的服务器。ServerSocket在调用accept()方法后,便会占据调用该方法的所在的线程。导致一次只能处理一次请求。我们可以通过多线程的方式来避免阻塞。如下:

 

上面的循环执行:
1.监听一个连接
2.一旦接收到一个连接的请求,就创建一个HandlerThread,并将接收到的socket发送给它。
3.启动处理请求的线程。
上面的代码对于低流量的服务器是足够应付的,但是一旦流量一大,就会出问题:
1.创建一个线程的花费太大
2.对于线程数目没有控制。所以,如果流量一大,则线程数会快速增长,导致服务器的资源被大量占用,使得服务器性能下降。
172473703
该架构的工作流程:
1.一个客户端向服务器发送请求。
2.服务端接收到这个请求后,将这个请求添加到一个请求队列中,然后接着去监听下一个请求。
3.这个请求队列将这个请求添加到一个内部的链式列表中(添加到列表的末尾,移除是从头部开始)
4.一个等待的请求线程被唤醒,从列表中提取一个请求,然后处理它。
5.这个请求线程将这个请求传递给一个请求处理器,该请求处理器直接与客户端进行连接。
这个理论是直截了当的,但是实现的细节优点麻烦。因为我们需要创建一系列的请求线程并强制它们在队列中等待一个新的 请求。这需要用到wait和notify来实现线程的管理和通信。

1.AbstractServer.java

AbstractServer使用RequestHandler的实现类名、创建请求线程的最小和最大数目以及允许队列增加的大小来初始化RequestQueue。当调用startServer()方法,它就调用ServerSocketFactory去创建一个新的ServerSocket,接着启动AbstracktServer的线程去调用线程的run方法。这个run()方法会用一个循环来调用ServerSocket的accept()方法,然后将接收到的socket添加到请求队列中。

2. RequestQueue.java

RequestQueue使用RequestThreads的最小线程数来初始化线程池,然后提供两个主要方法来与内部的链式队列进行交互:
add():将一个对象添加到队列的末尾
getNextObject():从队列的头部返回一个项目
使得RequestThreads与内部RequestQueue互相交互的关键是在getNextObject()方法中调用wait()方法。当RequestThread启动后,它就调用synchronized getNextObject() 方法去接收请求并处理请求。当请求队列是空的时候ugetNextObject()方法就调用wait()方法。当一个新的请求通过synchronized add()被添加到队列中,它就调用notifyAll()去唤醒这个线程,然后告诉它们去检查队列中这个新的请求。
至于synchronized的关键字的作用,我就跳过了。

 3. RequestThread.java

RequestThread创建一个i额RequestHandler的实例去委托处理请求。上面这个类核心公呢个是它的run()方法:它从队列中获取下一个对象,将自己标记为处理请求中,并将这个请求传递给RequestHandler, 移除自己身上的处理请求中的标记,然后尝试去队列中获取下一个对象。实际上,它只是将一个请求转发给RequestHandler而已。

4. RequestHandler.java

handlRequest方法用来处理请求。

Echo Server

最简单一种服务器就是echo server(回显)。一个echo server只是简单的将你所发送的内容返回,如果你向它发送字符串“abc”,它就会返回“abc”给你。对于高性能的echo server几乎没什么需求,但是因为它的简单,适合我们练手,描述如何从我们编写的框架来构建一个服务。

Listing 5. EchoServer.java

EchoServer的核心功能是在它的构造方法中调用父类的构造方法。这些选项创建了下面的行为:

  1.  echo server监听8899端口
  2.  RequestQueue ServerSockt在等待一个连接被添加到到RequestQueue的时候,可以持有50个 backlog 连接
  3. com.javasrc.server.echo.EchoRequestHandler是EchoServer的RequestHandler的实现
  4. The request queue can grow up to 2000 before rejecting new connections
  5. Initially, the RequestQueue creates five RequestThreads and the thread pool will never drop below five threads
  6. The thread pool can grow to a maximum of ten threads.

The AbstractServer class then handles creating the ServerSocket, listening on port 8899, and the request handling infrastructure to handle up to ten simultaneous echo clients. As each RequestThread processes its connection, it delegates the actual "conversation" with the client to the EchoRequestHandler class, shown in Listing 6.

Listing 6. EchoRequestHandler.java

The EchoRequestHandler's handleRequest() method is called by the RequestThread and passed the Socket to communicate with the client. The handlerRequest() method begins by obtaining a BufferedReader to read data from the client and a PrintWriter to write data back to the client. It then sits in a loop performing the following operations:

  1. Read a line from the client.
  2. Write that line back to the client (and flush the buffer to ensure that the client receives the line).
  3. Read the next line from the client.
  4. Repeat.

We always need a condition in a server to terminate the conversation. In this case, two conditions end the communication:

  • The client sends a blank line
  • The client closes the socket

Once the conversation is complete, the handleRequest() method returns. The RequestThread closes the socket and waits for the next request.

Although an echo server is very simplistic, the fact that we were able to create a very robust one (in terms of quickly handling several simultaneous connections) in under 200 lines of well-documented code is impressive.

Chat Server

A chat server is an interesting server. I was first involved in constructing one in 1998, while working for an online video game company. The purpose of a chat server is to enable clients to connect and chat with one another. 一个基本的聊天服务器提供下面的特性:

  • 一个用户可以使用一个唯一的用户名登录
  • 当有新的用户登录后,所有其他用户会收到提醒
  • 一个用户可以给所有的用户发送信息
  • 一个用户可以发送私有信息给某个特定的用户
  • 一个用户可以发送表情给所有的用户
  • 一个用户可以退出聊天
  • 当一个用户离开后,其他用户可以收到提醒

To implement this functionality in the JavaSRC server framework, we build three classes: ChatServer, ChatRequestHandler, and DuplicateLoginException. While the echo server completely disconnected the server from the request handler, the chat server needs a much tighter coupling between the two.

The request handler represents a chat client. It needs to tell the chat server to send messages to one or more other chat clients. Plus, the chat server needs to know about all clients to facilitate this communication and to report management functionality (such as current users and user joining and exiting events). To facilitate this, the ChatServer provides a static reference to its instance that request handlers can use to register themselves with.

Listing 7 shows the code for the ChatServer class.

Listing 7. ChatServer.java

The constructor creates a server listening on port 9988 with a socket backlog of 50 connections, a request queue that can grow up to 2000 requests, 5 initial and minimum request threads, and 50 maximum request threads. Request handling is delegated to the ChatRequestHandler class.

Because the ChatServer is the main conduit between chat clients, it maintains references to them in a private chatClients map and exposes the following methods for them to use:

  • addChatClient(): Adds the specified chat client to the chat server and notify all existing clients that this new client has arrived; duplicate logins are forbidden.
  • sendMessage(): Sends a message from a client to all other chat clients.
  • sendMessage() (from and to names): Sends a private message from a client to another client.
  • sendEmotion(): Sends an emotion from a client to all other clients.
  • getUsers(): Returns a Set containing the names of all currently logged on users.
  • removeChatClient(): Removes the specified client from the chat server and notifies all other clients that this user has exited.

Socket communications with chat clients are handled in the ChatRequestHandler class, shown in listing 8.

Listing 8. ChatRequestHandler.java

The handleRequest() method is the entry-point into this class from the RequestThread. It operates as follows:

  1. Obtain a BufferedReader and PrintWriter with which to communicate with the client.
  2. Read a line from the client.
  3. Extract a 4-character command from the line.
  4. Pass the command name and the line to the handleCommand() method.
  5. Read the next line from the client, and
  6. Repeat.

The exit condition for this server, as we'll see later, is a boolean condition set by the result of the handleCommand() method. The exit condition is satisfied when the user enters the command EXIT.

The handleCommand() method implements the logic of the chat server. The following summarizes the commands that it supports:

  • USER: logs in a new user by asking the ChatServer to add a new client; this validates that the user's name is not already in use.用于用户切换帐号登录
  • HELP: displays the help menu (writes directly back to the socket).
  • EXIT: asks the ChatServer to remove this user from its chat client list and then returns false to cause the handleRequest() loop to cease.
  • SEND: sends a message to all users in the chat server by calling the ChatServer's sendMessage() method.
  • PRIV: sends a private message to a specific user by calling an alternate version of the ChatServer's sendMessage() method.
  • LIST: retrieves a list of the current users in the chat server by calling the ChatServer's getUsers() method.打印出所有在线的用户
  • EMOT: sends an emotion to all users in the chat server by calling the ChatServer's sendEmotion() method.

And then the ChatRequestHandler also handles notifications of these messages from the ChatServer, generated by other ChatRequestHandler instances. The following summarizes these notifications:

  • sendMessage(): sends the message to our user; the notification command is MESG.
  • sendEmotion(): tells our user that the specified user has sent an emotion to the group; the notification command is EMOT.
  • privateMessage(): sends the private message to our user; the notification command is PRIV.
  • newUser(): notifies our user that the specified new user has just joined the chat; the notification command is USER.
  • removeUser(): notifies our user that the specified user has left the chat; the notification command is RUSR.

For completeness, listing 9 shows the code for the DuplicateLoginException class.

Listing 9. DuplicateLoginException.java

RequestQueueException.java

因为我们还没一个聊天客户端,测试聊天服务器的最好的方式是使用telnet。下面是一个与聊天服务器的简单的telnet会话。注意,当你使用telnet连接的服务器的时候,必须指定端口;否则它会连接默认端口(25)

And the conversation is as follows:

In this example, the bold commands are the ones that I sent; the non-bold ones are responses and notifications. In this conversation, "userOne" logged on, listed the current users, sent a message, received a message, sent a private message, received a private message, sent an emotion, saw that "userTwo" left, and then exited. It’s a simple conversation that could be presented very well inside of a robust chat client, but it suffices for demonstration purposes.

Summary

Thus far we have looked at creating the infrastructure for a robust Java server, which inclues the introduction of a request queue that is serviced by a set of request threads, and then built two examples on top of this infrastructure: a simple echo server and a more complex chat server. These examples illustrate that the JavaSRC server framework allows you to focus more on business logic and less on the implementation details behind building a robust server.

附上自己写的一个聊天客户端

分析聊天客户端功能:
1.接收服务器发回的信息
2.获取用户的键盘输入,发送给服务器。
关键点分析:
因为接收服务器信息是个流,假设我们使用下面的方式打印客户端接收到的信息:
那么,while的判断条件是永远无效的,永远也不会跳出while循环。因为这种方式是阻塞的,即使输入流已经读取了所有的数据,但是它依然还是会一直阻塞,等待服务端发送的信息。所以会阻塞在那,这样我们就无法获取用户键盘输入的信息。
解决的办法:为获取用户的键盘输入这个功能单独开一个线程,这样,即使输入流阻塞了,用户也可以照样输入。
方法
demo如下:
总结:其实,也可以只使用一个线程来处理客户端的用户输入和输出。这样的话,为了避免阻塞,就必须跳出while循环。就只能是定义一个“EOF”用来表示每次信息传输完毕。或者是告诉客户端每次发送消息的大小。就是说,必须提供一种方式来跳出while循环,然后去执行获取用户输入的代码。

 

转载请注明:大步's Blog » 构建一个robust的java 服务器(部分翻译+附上自己写的客户端)

SiteMap