5. Lớp ServerSocket

Lớp ServerSocket có  đủ mọi thứ ta cần  để viết các server bằng Java. Nó có các constructor để tạo các đối tượng ServerSocket mới, các phương thức để lắng nghe các liên kết trên một cổng xác định, và các phương thức trả về một Socket khi liên kết được thiết lập, vì vậy ta có thể gửi và nhận dữ liệu.

Vòng đời của một server

1. Một ServerSocket mới được tạo ra trên một cổng xác định bằng cách sử dụng một constructor ServerSocket.

2. ServerSocket lắng nghe liên kết đến trên cổng đó bằng cách sử dụng phương thức accept(). Phương thức accept() phong tỏa cho tới khi một client thực hiện một liên kết, phương thức accept() trả về một  đối tượng Socket mà liên kết giữa client và server.

3. Tùy  thuộc vào kiểu server, hoặc phương thức getInputStream(), getOutputStream() hoặc cả hai được gọi để nhận các luồng vào ra để truyền tin với client.

4.  server và client tương tác theo một giao thức thỏa thuận sẵn cho tới khi ngắt liên kết.

5.  Server, client hoặc cả hai ngắt liên kết

6. Server trở về bước hai và đợi liên kết tiếp theo.

5.1. Các constructor

•  public ServerSocket(int port) throws IOException, BindException
Constructor này tạo một socket cho server trên cổng xác định. Nếu port bằng 0, hệ thống chọn một cổng ngẫu nhiên cho ta. Cổng do hệ thống chọn đôi khi được gọi là cổng vô danh vì ta không biết số hiệu cổng. Với các server, các cổng vô danh không hữu ích lắm vì các client cần phải biết trước cổng nào mà nó nối tới (giống như người gọi điện thoại ngoài việc xác định cần gọi cho ai cần phải biết số điện thoại để liên lạc với người đó).

Ví dụ: Để tạo một server socket cho cổng 80

try{
ServerSocket httpd = new ServerSocket(80);
}catch(IOException e) {
System. err.println(e);
}

Constructor đưa ra ngoại lệ IOException nếu ta không thể tạo và gán Socket cho cổng được yêu cầu. Ngoại lệ IOException phát sinh khi:

• Cổng đã được sử dụng
•  Không có quyền hoặc cố  liên kết với một cổng nằm giữa 0 và 1023.

Ví dụ;

import java.net.*;
import java.io.*;

public class congLocalHost
{
public static void main(String[] args)
{
ServerSocket ss;

for(int i=0;i<=1024;i++)  {
try{
ss= new ServerSocket(i);
ss.close();
} catch(IOException e) {
System.out.println(“Co mot server tren cong “+i);
}
}
}
}

•  public ServerSocket(int port, int queuelength, InetAddress bindAddress)throws IOException  Constructor này tạo một đối tượng ServerSocket trên cổng xác định với chiều dài hàng đợi xác định. ServerSocket chỉ gán cho địa chỉ IP cục bộ xác định. Constructor này hữu ích cho các server chạy trên các hệ thống có nhiều địa chỉ IP.

5.2. Chấp nhận và ngắt liên kết

Một đối tượng ServerSocket hoạt động trong một vòng lặp chấp nhận các liên kết. Mỗi lần lặp nó gọi phương thức accept(). Phương thức này trả về một đối tượng Socket biểu diễn liên kết giữa client và server. Tương tác giữ client và server được tiến hành thông qua socket này. Khi giao tác hoàn thành, server gọi phương thức close() của đối tượng socket.

Nếu client ngắt liên kết trong khi server vẫn đang hoạt động, các luồng vào ra kết nối server với client sẽ đưa ra ngoại lệ InterruptedException trong lần lặp tiếp theo.

•  public Socket accept() throws IOException
Khi bước thiết lập liên kết hoàn thành, và ta sẵn sàng để chấp nhận liên kết, cần gọi phương thức accept() của lớp ServerSocket. Phương thức này phong tỏa; nó dừng quá trình xử lý và đợi cho tới khi client được kết nối. Khi client thực sự kết nối, phương thức accept() trả về  đối tượng Socket. Ta sử dụng các phương thức getInputStream() và getOutputStream() để truyền tin với client.

Ví dụ:

try{
ServerSocket theServer = new ServerSocket(5776);
while(true) {

Socket con = theServer.accept();
PrintStream p = new PrintStream(con.getOutputStream());
p.println(“Ban da ket noi toi server nay. Bye-bye now.”);
con.close();
}
}catch(IOException e) {
System.err.println(e);
}

•  public void close() throws IOException
Nếu ta đã kết thúc làm việc với một đối tượng server socket thì cần phải đóng lại đối tượng này.

Ví dụ: Cài đặt một server daytime

import java.net.*;
import java.io.*;
import java.util.Date;

public class daytimeServer{

public final static int daytimePort =13;

public static void main(String[]args) {
ServerSocket  theServer;
Socket con;
PrintStream p;
try{
theServer = new ServerSocket(daytimePort);
try{

p= new PrintStream(con.getOutputStream());
p.println(new Date());
con.close();
}catch(IOException e) {
theServer.close();
System. err.println(e);
}
}catch(IOException e) {
System. err.println(e);
}
}
}

•  public void close() throws IOException
Nếu đã hoàn thành công việc với một ServerSocket, ta cần phải đóng nó lại, đặc biệt nếu chương trình của ta tiếp tục chạy. Điều này nhằm tạo điều kiện cho các chương trình khác muốn sử dụng nó. Đóng một ServerSocket không đồng nhất với việc đóng một Socket.
Lớp ServerSocket cung cấp một số phương thức cho ta biết địa chỉ cục bộ và cổng mà trên đó đối tượng server đang hoạt động. Các phương thức này hữu ích khi ta đã mở một đối tượng server  socket trên một cổng vô danh và trên một giao tiếp mạng không

•  public InetAddress getInetAddress()
Phương thức này trả về địa chỉ được sử dụng bởi server (localhost). Nếu localhost có địa chỉ IP, địa chỉ này được trả về bởi phương thức InetAddress.getLocalHost().

Ví dụ:

try{

ServerSocket httpd = new ServerSocket(80);
InetAddress ia = httpd.getInetAddress();

} catch(IOException e) {}

•  public int getLocalHost()
Các contructor ServerSocket cho phép ta nghe dữ liệu trên cổng không  định trước bằng cách gán số 0 cho cổng. Phương thức này cho phép ta tìm ra cổng mà server đang nghe.

6. Các bước cài đặt chương trình phía Client bằng Java

Sau khi đã tìm hiểu các lớp và các phương thức cần thiết để cài đặt chương trình Socket. Ở mục 6 và mục 7 chúng ta sẽ đi vào các bước cụ thể để cài đặt các chương trình Client và Server.

Các bước để cài đặt Client

• Bước 1:Tạo một đối tượng Socket
Socket client =new Socket(“hostname”,portName);

• Bước 2:Tạo một luồng xuất để có thể sử dụng để gửi thông tin tới Socket
PrintWriter out=new PrintWriter(client.getOutputStream(),true);

• Bước 3:Tạo một luồng nhập để đọc thông tin đáp ứng từ server
BufferedReader in=new BufferedReader(new InputStreamReader(client.getInputStream()));

• Bước 4:Thực hiện các thao tác vào/ra với các luồng nhập và luồng xuất
Đối với các luồng xuất, PrintWriter, ta sử dụng các phương thức print và println, tương tự như System.out.println. Đối với luồng nhập, BufferedReader, ta có thể sử dụng phương thức read() để đọc một ký tự, hoặc một mảng các ký tự, hoặc gọi phương thức readLine() để đọc vào một dòng ký tự. Cần chú ý rằng phương thức readLine() trả về null nếu kết thúc luồng.

• Bước 5: Đóng socket khi hoàn thành quá trình truyền tin
Ví dụ: Viết chương trình client liên kết với một server. Người sử dụng nhập vào một dòng ký tự  từ bàn phím và gửi dữ liệu cho server.

import java.net.*;
import java.io.*;

public class  EchoClient1 {

public static void main(String[] args) {

String hostname=”localhost”;
if(args.length>0) {
hostname=args[0];
}

PrintWriter pw=null;
BufferedReader br=null;
try{

Socket s=new Socket(hostname,2007);
br=new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedReader  user=new  BufferedReader(new InputStreamReader(System.in));
pw=new PrintWriter(s.getOutputStream());
System.out.println(“Da ket noi duoc voi server…”);

while(true) {
String st=user.readLine();

if(st.equals(“exit”)) {
break;
}
pw.println(st);
pw.flush();
System.out.println(br.readLine());
}
} catch(IOException e) {
System.err.println(e);
} finally{

try{

if(br!=null)br.close();
if(pw!=null)pw.close();

} catch(IOException e) {
System.err.println(e);
}
}
}
}

Chương trình EchoClient đọc vào hostname từ đối dòng lệnh. Tiếp theo ta tạo một socket với hostname đã xác định trên cổng số 2007. Tất nhiên cổng này hoàn toàn do ta lựa chọn sao cho nó không trùng với cổng đã có dịch vụ hoạt động. Việc tạo socket thành công có nghĩa là ta đã liên kết được với server. Ta nhận luồng nhập từ socket thông qua phương thức getInputStream() và gắn kết nó với các luồng ký tự  và luồng đệm nhờ lệnh:
br=new BufferedReader(new InputStreamReader(s.getInputStream());

Tương tự ta lấy về luồng xuất thông qua phương thức getOuputStream() của socket. Sau đó gắn kết luồng này với luồng PrintWriter để gửi dữ liệu tới server:
pw=new PrintWriter(s.getOutputStream());

Để đọc dữ liệu từ bàn phím ta gắn bàn phím với các luồng nhập nhờ câu lệnh:
BufferedReader user=new BufferedReader(new InputStreamReader(System.in));

Sau đi đã tạo được các luồng thì vấn đề nhận và gửi dữ liệu trở thành vấn đề đơn giản là đọc dữ liệu từ các luồng nhập br, user và ghi dữ liệu lên luồng xuất pw.

7. Các bước để cài đặt chương trình Server bằng Java

Để cài đặt chương trình Server bằng ServerSocket ta thực hiện các bước sau:

• Bước 1
Tạo một đối tượng ServerSocket
ServerSocket ss=new ServerSocket(port)

• Bước 2:
Tạo một  đối tượng Socket bằng cách chấp nhận liên kết từ yêu cầu liên kết của client. Sau khi chấp nhận liên kết, phương thức accept() trả về đối tượng Socket thể hiện liên kết giữa Client và Server.

while(condion) {

Socket s=ss.accept();
doSomething(s);

}

Người ta khuyến cáo rằng chúng ta nên giao công việc xử lý  đối tượng s cho một tuyến đoạn nào đó.

• Bước 3: Tạo một luồng nhập để đọc dữ liệu từ client
BufferedReader in=new BufferedReader(new InputStreamReader(s.getInputStream()));

• Bước 4: Tạo một luồng xuất để gửi dữ liệu trở lại cho server
PrintWriter pw=new PrintWriter(s.getOutputStream(),true);
Trong đó tham số true được sử dụng để xác định rằng luồng sẽ được tự động đẩy ra.

• Bước 5: Thực hiện các thao tác vào ra với các luồng nhập và luồng xuất.

• Bước 6: Đóng socket s khi đã truyền tin xong. Việc đóng socket cũng đồng nghĩa với việc đóng các luồng.

Ví dụ: Viết chương trình server EchoServer để phục vụ chương trình EchoClient1 đã viết ở bước 5

import java.net.*;
import java.io.*;

public class  EchoServer1 {

public final static int DEFAULT_PORT=2007;

public static void main(String[] args) {
int port=DEFAULT_PORT;
try{

ServerSocket ss=new ServerSocket(port);
Socket s=null;

while(true) {
try{
s=ss.accept();
PrintWriter  pw=new  PrintWriter(new OutputStreamWriter(s.getOutputStream()));                BufferedReader  br=new  BufferedReader(new InputStreamReader(s.getInputStream()));

while(true){
String line=br.readLine();
if(line.equals(“exit”))break;
String upper=line.toUpperCase();
pw.println(upper);
pw.flush();
}
} catch(IOException e) { }
finally{

try{

if(s!=null){
s.close();
}
} catch(IOException e){}
}
}
} catch(IOException e) { }
}
}

Chương trình bắt  đầu bằng việc tạo ra một  đối tượng ServerSocket trên cổng xác định. Server lắng nghe các liên kết trong một vòng lặp vô hạn. Nó chấp nhận liên kết bằng cách gọi phương thức accept(). Phương thức accept() trả về một đối tượng Socket thể hiện mối liên kết giữa client và server. Ta cũng nhận về các luồng nhập và luồng xuất từ  đối tượng Socket nhờ các phương thức getInputStream() và getOuputStream(). Việc nhận yêu cầu từ client sẽ thông qua các luồng nhập và việc gửi đáp ứng tới server sẽ thông qua luồng xuất.

8. Ứng dụng đa tuyến đoạn trong lập trình Java

Các server  như đã viết ở trên rất đơn giản nhưng nhược điểm của nó là bị hạn chế về mặt hiệu năng vì nó chỉ quản lý được một client tại một thời điểm. Khi khối lượng công việc mà server cần xử lý một yêu cầu của client là quá lớn và không biết trước được thời điểm hoàn thành công việc xử lý thì các server này là không thể chấp nhận được.

Để khắc phục điều này, người ta quản lý mỗi phiên của client bằng một tuyến đoạn riêng, cho phép các server làm việc với nhiều client đồng thời. Server này được gọi là server tương tranh (concurrent server)-server tạo ra một tuyến đoạn để quản lý từng yêu cầu, sau đó tiếp tục lắng nghe các client khác.

Chương trình client/server chúng ta  đã xét mở mục 6 và mục 7 là chương trình client/server đơn tuyến đoạn. Các server đơn tuyến đoạn chỉ quản lý được một liên kết tại một thời điểm. Trong thực tế một server có thể phải quản lý nhiều liên kết cùng một lúc. Để thực hiện điều này server chấp nhận các liên kết và chuyển các liên kết này cho từng tuyến đoạn xử lý.

Trong phần dưới đây chúng ta sẽ xem xét cách tiến hành cài đặt một chương trình client/server đa tuyến đoạn.

Chương trình phía server

import java.io.*;
import java.net.*;

class EchoServe extends Thread {

private Socket socket;
private BufferedReader in;
private PrintWriter out;

public EchoServe (Socket s) throws IOException {

socket = s;
System.out.println(“Serving: “+socket);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

// Cho phép  auto-flush:
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(  socket.getOutputStream())), true);

// Nếu bất kỳ lời gọi nào ở trên đưa ra ngoại lệ
// thì chương trình gọi có trách nhiệm đóng socket. Ngược lại tuyến đoạn sẽ
// sẽ đóng socket
start();
}

public void run() {

try {

while (true) {
System.out.println(“….Server is waiting…”);
String str = in.readLine();
if (str.equals(“exit”) ) break;
System.out.println(“Received: ” + str);
System.out.println(“From: “+ socket);
String upper=str.toUpperCase();
// gửi lại cho client
out.println(upper);
}
System.out.println(“Disconnected with..”+socket);
}  catch (IOException e) {}
finally  {
try  {
socket.close();
}  catch(IOException e) {}
}
}
}

public class TCPServer1 {

static int PORT=0; .
public static void main(String[] args) throws IOException {

if (args.length == 1) {
PORT=Integer.parseInt(args[0]); // Nhập số hiệu cổng từ đối dòng lệnh
}

// Tạo một đối tượng Server Socket
ServerSocket s = new ServerSocket(PORT);
InetAddress  addrs= InetAddress.getLocalHost();
System.out.println(“TCP/Server running on : “+ addrs +” ,Port “+s.getLocalPort());
try  {
while(true)
{
// Phong tỏa cho tới khi có một liên kết đến
Socket socket = s.accept();
try  {
new EchoServe(socket);  // Tạo một tuyến đoạn quản lý riêng từng liên kết
} catch(IOException e) {
socket.close();
}
}
}  finally {
s.close();
}
}
}

Chương trình phía client

import java.net.*;
import java.io.*;

public class TCPClient1  {

public static void main(String[] args) throws IOException
{

if (args.length != 2)  {
System.out.println(“Sử dụng: java TCPClient  hostid port#”);
System.exit(0);
}
try
{
InetAddress addr = InetAddress.getByName(args[0]);
Socket socket = new Socket(addr, Integer.parseInt(args[1]));
try   {
System.out.println(“socket = ” + socket);
BufferedReader in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed by PrintWriter:
PrintWriter out =new PrintWriter(new BufferedWriter( new      OutputStreamWriter(socket.getOutputStream())),true);
// Đọc dòng ký tự từ bàn phím
DataInputStream  myinput  =  new  DataInputStream(new BufferedInputStream(System.in));
try  {
for(;;) {
System.out.println(“Type  anything  followed  by  RETURN,  or  Exit  to
terminate the program.”);
String strin=myinput.readLine();
// Quit if the user typed ctrl+D
if (strin.equals(“exit”)) break;
else
out.println(strin);          // Send the message
String strout = in.readLine();      // Recive it back
if ( strin.length()==strout.length()) {  // Compare Both Strings       System.out.println(“Received: “+strout);
}  else
System.out.println(“Echo bad — string unequal”+ strout);
} // of for ;;
} catch (IOException e) {
e.printStackTrace(); }
// User is exiting
}   finally   {
System.out.println(“EOF…exit”);
socket.close();
}
}   catch(UnknownHostException e)  {
System.err.println(“Can’t find host”);
System.exit(1);
}   catch (SocketException e)  {
System.err.println(“Can’t open socket”);
e.printStackTrace();
System.exit(1);
}
}
}

9. Kết luận

Chúng ta đã tìm hiểu cách lập trình mạng cho giao thức TCP. Các Socket còn được gọi là socket luồng vì để gửi và nhận dữ liệu đều được tiến hành thông qua việc đọc ghi các luồng. Ta đọc cũng đã tìm hiểu cơ chế hoạt động của socket và cách thức lập các chương trình server và client. Ngoài ra, chương này cũng đã giải thích tạo sao cần có cài đặt server đa tuyến đoạn và  tìm hiểu cách thức để lập các chương trình client/server đa tuyến đoạn.

Trong chương tiếp theo chúng ta sẽ học cách xây dựng một chương trình client/server cho giao thức UDP, một giao thức gần với giao thức TCP.

Các bài liên quan:
Lập trình mạng trong JAVA(Phần 6)