Java TCP程序设计 在Java
中使用Socket
(即套接字)完成TCP
程序的开发,使用此类可以方便的建立可靠的,双向的,持续的,点对点的通信连接。 在Socket
的程序开发中,服务器段使用SeverSocket
等待客户端的连接,对于Java
的网络编程,每一个客户端都使用一个Socket
对象表示,如下图所示。
SeverSocket类与Socket类 SeverSocket
类主要用于在服务器端程序的开发上,用于接收客户端的连接请求。SeverSocket
类的常用方法如下表所示。
序号
方法
描述
1
ServerSocket(int port)
创建SeverSocket实例,并制定监听窗口
2
Socket accept()
侦听并接受到此套接字的连接。等待客户端的连接,此方法之前一直阻塞
3
InetAddress getInetAddress()
返回服务器的IP地址
4
boolean isClosed()
返回 ServerSocket 的关闭状态。
5
void close()
关闭此套接字。
在服务器端每次运行时都要使用accept()
方法等待客户端连接,此方法发执行之后服务器段将进入到阻塞状态,直到客户端连接之后程序才能向下继续执行。此方法的返回值是Socket
,每一个Socket
都表示一个客户端对象,Socket
的常用方法如下表所示。
序号
方法
描述
1
Socket(String host, int port)
创建一个流套接字并将其连接到指定主机上的指定端口号。
2
InputStream getInputStream()
返回此套接字的输入流。
3
OutputStream getOutputStream()
返回此套接字的输出流。
4
boolean isClosed()
判断此套接字是否被关闭
5
void close()
关闭此套接字。
  在客户端,程序可以通过Socket
类的getInputStream()
方法取得服务器的输出信息,在服务端可以通过getOutputStream()
方法取得客户端的输出信息,如下图所示: 在网络编程中需要使用输入及输出流的形式完成信息的传递,所以在开发时需要导入java.io
包。
第一个TCP程序   下面通过ServerSocket
类及Socket
类完成 一个服务器的程序开发,此服务向客户端输出”hello
world
!”的字符串信息。
服务端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package my.net.tcp;import java.net.*;import java.io.*;public class HelloServer { public static void main (String args[]) throws Exception { ServerSocket server = null ; Socket client = null ; PrintStream out = null ; server = new ServerSocket (8888 ); System.out.println("服务器运行,等待客户端连接。" ); client = server.accept(); String str = "hello world" ; out = new PrintStream (client.getOutputStream()); out.println(str); client.close(); server.close(); } }
客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package my.net.tcp;import java.net.*;import java.io.*;public class HelloClient { public static void main (String args[]) throws Exception { Socket client = null ; client = new Socket ("localhost" , 8888 ); BufferedReader buf = null ; buf = new BufferedReader ( new InputStreamReader (client.getInputStream())); String str = buf.readLine(); System.out.println("接收到服务器端输出内容:" + str); buf.close(); client.close(); } }
编码问题 上面的代码在eclipse
中运行控制台输出可能有问题,所以到命令行中去编译和运行。因为用的编码是UTF-8
编码的,所以编译的时候要指定编码。 如果不指定编码的话,会造成无法编译:
1 HelloServer.java:10: 错误: 编码GBK的不可映射字符
CMD 中编译运行服务端 编译服务端:
1 javac -encoding UTF-8 -d . HelloServer.java
运行服务端:
1 java my.net.tcp.HelloServer
此时的运行结果: 可以看到服务器正在等待客户端的连接,如果没有收到客户端的连接,服务器端的将一直等待客户端的连接。
CMD 中编译运行客户端 编译客户端:
1 javac -encoding utf-8 -d . HelloClient.java
运行客户端:
1 java my.net.tcp.HelloClient
客户端连接上了之后,服务端程序才能接着往下运行,此时,服务端和客户端的运行结果如下。服务端运行结果: 客户端运行结果:
从上面的程序的运行结果可以发现,服务器程序一执行到accept()方法后,程序将进入阻塞状态,直到客户端连接上之后,服务器才会继续往下执行。
使用telnet命令连接服务器端进行验证 服务器端程序建立完成之后,因为这里使用的是TCP
通信协议连接的,所以可以直接使用telnet
即可取得服务器的输出信息。 步骤: 在命令行下输入telnet
即可进入telnet
:
重新打开一个cmd
窗口,进入到服务端所在的目录,然后启动客户端:
1 java my.net.tcp.HelloServer
客户端输出:
然后切换到telnet命令行窗口,输入:
telnet运行结果: 可以看到telnet成功连接到服务器端,此时telnet为客户端,服务器端向telnet客户端输出”hello world”服务端运行结果: 可以看到telnet
连接到服务端之后,服务器停止监听,继续往下运行。打印一条信息后服务端后停止运行。
telnet运行不正正常的情况: 没有开启telnet服务 如果还没有开启telnet服务的话,是无法启动telnet的。输入telnet显示如下的错误信息: 解决方法是启动telnet即可
如何启动telnet 首先进入控制面板: 然后在控制面板的收缩框中输入:启用或关闭Windows功能
,然后点击启用或关闭Windows功能
进入 然后下拉滚动条,找到Telnet客户端,在前面打钩,然后点确定即可启动Telnet客户端。
TCP实例:Echo程序 Echo
程序是一个网络编程通信交互的一个经典案例,称为回应程序,即客户端输入什么内容,服务器端就会在这些内容前面加上”Echo:
“,然后把这些信息发回给客户端,下面实现这样的一个程序。 在上面的代码中,服务器端每次执行完毕后服务器都会退出,这是因为服务器端指定接收一个客户端的连接,主要是由于accpet()
方法只能使用一次。下面的程序中将通过循环的方式使用accpt()
,这样每一个客户端执行完毕后,服务器端都可以重新等待用户连接。
实例:EchoServer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package my.net.tcp;import java.net.*;import java.io.*;public class EchoServer { public static void main (String args[]) throws Exception { ServerSocket server = null ; Socket client = null ; BufferedReader buf = null ; PrintStream out = null ; server = new ServerSocket (6666 ); boolean f = true ; while (f) { System.out.println("服务器运行,等待客户端连接。" ); client = server.accept(); out = new PrintStream (client.getOutputStream()); buf = new BufferedReader ( new InputStreamReader (client.getInputStream())); boolean flag = true ; while (flag) { String str = buf.readLine(); if (str == null || "" .equals(str)) { flag = false ; } else { if ("exit" .equals(str)) { flag = false ; } else { out.println("ECHO : " + str); } } } client.close(); } server.close(); } }
程序运行结果: 可以看到服务器运行之后,和之前的一样,要等待客户端的连接。下面是客户端程序的代码。
实例:EchoClient 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package my.net.tcp;import java.net.*;import java.io.*;public class EchoClient { public static void main (String args[]) throws Exception { Socket client = null ; client = new Socket ("localhost" , 6666 ); BufferedReader buf = null ; PrintStream out = null ; BufferedReader input = null ; input = new BufferedReader (new InputStreamReader (System.in)); buf = new BufferedReader ( new InputStreamReader (client.getInputStream())); out = new PrintStream (client.getOutputStream()); boolean flag = true ; while (flag) { System.out.print("输入信息:" ); String str = input.readLine(); out.println(str); if ("exit" .equals(str)) { flag = false ; } else { String echo = buf.readLine(); System.out.println(echo); } } buf.close(); client.close(); }
在客户端EchoClient.java目录下,重新打开一个cmd窗口,然后编译运行客户端。运行结果: 从程序运行的结果中可以发现,所有的输入信息最终都会通过回显的方式发回给客户端,并且在前面加上了”ECHO:
“的信息。另外在本程序用,当一个客户端结束之后服务器端并并不会退出,而是继续等待下一个用户连接,下一个用户连接上了之后,服务器端会继续执行。 但是在本程序中有一个问题,就是现在的服务器端每次只能有一个用户能连接,属于单线程的处理机制,如果现在有其他客户端要连接到客户端,由于现在已经有一个客户端连接到服务器端了,其他客户端是无法连接的,要等待服务器出现空闲才可以连接。
为了能保证服务器可以同时连接多个客户端,可以加入多线程机制,即每一个客户端连接之后都启动一个线程,这样一个服务器就可以同时支持多个客户端的连接。
在服务器上应用多线程 对于服务器端来说,如果要加入多线程机制,则应该在每个用户连接之后启动一个新的线程提供服务。下面先建立一个EchoThread类,EchoThread类是专门用于提供服务的多线程操作,这里的多线程使用Runnable接口的方式实现。 把EchoSever.java中的accpet()之后的服务代码打包成线程,这样客户端连接上之后服务器就启动一个线程为该客户端提供服务,然后等待下一个客户端的连接。这样就能接收多个客户端的连接,并同时为多个客户端提供服务。 具体的做法是,把EchoSever.java中的accpet()之后的提供服务的代码,放到EchoThread.java的run()方法中。
实例:EchoThread 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package my.net.tcp;import java.net.*;import java.io.*;public class EchoThread implements Runnable { private Socket client = null ; public EchoThread (Socket client) { this .client = client; } public void run () { BufferedReader clientIn = null ; PrintStream outToClient = null ; try { outToClient = new PrintStream (client.getOutputStream()); clientIn = new BufferedReader ( new InputStreamReader (client.getInputStream())); boolean flag = true ; while (flag) { String str = clientIn.readLine(); if (str == null || "" .equals(str)) { flag = false ; } else { if ("exit" .equals(str)) { flag = false ; } else { outToClient.println("ECHO : " + str); } } } client.close(); } catch (Exception e) { } } }
EchoThread.java的主要功能就是接受一个客户端的Socket,并通过循环的方式接受客户端的输入信息,然后向客户端回送刚才输入的信息。 下面创建EchoThreadServer类,使用上面的EchoThread来为客户端提供服务。
实例:EchoThreadServer类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package my.net.tcp;import java.net.*;public class EchoThreadServer { public static void main (String args[]) throws Exception { ServerSocket server = null ; Socket client = null ; server = new ServerSocket (6666 ); boolean f = true ; while (f) { System.out.println("服务器运行,等待客户端连接。" ); client = server.accept(); new Thread (new EchoThread (client)).start(); } server.close(); } }
还是对比上面的EchoServer.java,我们发现把EchoServer.java的接受连接的部分 和提供服务功能部分 分割出来成两个类,即可得到接受连接部分EchoThreadServer.java,和提供服务功能部分:EchoThread.java
运行服务端EchoThreadServer
然后运行客户端:EchoClient 此时服务器端程序会创建一个服务进程来提供服务,然后进入下一个循环,等待下一个客户端的连接 再打开一个cmd窗口,然后启动另一个客户端: 可以看到此时已经成功连接上两个客户端了,这两个客户端都正常运行,且服务器端已经准备等待第三个客户端的连接。这样,在服务端,每一个连接到服务器的客户端Socket都会以一个线程的方式运行,无论有多少个客户端连接都可以同时完成操作。
使用回调函数 如果想知道到底有多少个客户端正在和服务器进行通讯,可以通过回调函数来实现.
回调函数 假设A线程创建了B线程,B线程运行过程中调用了A线程的方法,则该方法就叫回调方法.
如何通过客户端关闭服务器 EchoThreadServer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package tcp;import java.net.*;public class EchoThreadServer { private static boolean isServerAlive = true ; private static int clientNum = 0 ; public static void main (String args[]) throws Exception { ServerSocket server = null ; Socket client = null ; server = new ServerSocket (6666 ); while (isServerAlive) { System.out.println("等待客户端连接..." ); client = server.accept(); System.out.println(" 客户端连接成功,当前客户端数量:" + clientNum); if (isServerAlive) { new Thread (new EchoThread (client)).start(); } } server.close(); client.close(); System.out.println("服务端已经停止..." ); } public static void addClientNum () { clientNum = clientNum + 1 ; } public static void minusClientNum () { clientNum = clientNum - 1 ; } public static int getClientNum () { return clientNum; } public static void shutdownServer () { isServerAlive = false ; } public static boolean isServerAlive () { return isServerAlive; } }
EchoThreadServer
这个类中的,minusClientNum
,getClientNum
,shutdownServer
这三个方法都是给子线程调用的方法,也就是所谓的回调方法.
EchoThread 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package tcp;import java.net.*;import java.io.*;public class EchoThread implements Runnable { private Socket client = null ; int clienId; public EchoThread (Socket client) { this .client = client; this .clienId = EchoThreadServer.getClientNum(); EchoThreadServer.addClientNum(); } public void run () { BufferedReader inByclient = null ; PrintStream outToClient = null ; try { outToClient = new PrintStream (client.getOutputStream()); inByclient = new BufferedReader ( new InputStreamReader (client.getInputStream())); boolean isClientAlive = true ; while (isClientAlive) { String str = inByclient.readLine(); if ("exit" .equals(str)) { isClientAlive = false ; EchoThreadServer.minusClientNum(); } else if ("shutdownServer" .equals(str)) { EchoThreadServer.shutdownServer(); System.out.println("关闭服务器" ); break ; } else { outToClient.println( "服务线程 " + this .clienId + " ECHO to client: " + str); } } } catch (Exception e) { } } }
当EchoThread
的一个线程启动时,将会调用主线程的addClientNum
方法对主线程中的计数器加1
,该线程结束时调用minusClientNum
方法对主线程中的计数器减1
. 当服务线程收到shutdownServer
这个字符串的时候,将调用主线程的shutdownServer
方法关闭服务器的主线程,这样服务器就不能响应新的客户端连接了.等到之前连接过的客户端都结束后客户端将会真正结束。
EchoClient 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package tcp;import java.net.*;import java.io.*;public class EchoClient { public static void main (String args[]) throws Exception { Socket client = null ; client = new Socket ("localhost" , 6666 ); BufferedReader inputByServer = null ; PrintStream outToServer = null ; BufferedReader input = null ; input = new BufferedReader (new InputStreamReader (System.in)); inputByServer = new BufferedReader ( new InputStreamReader (client.getInputStream())); outToServer = new PrintStream (client.getOutputStream()); boolean flag = true ; while (flag && EchoThreadServer.isServerAlive()) { System.out.print("输入信息:" ); String str = input.readLine(); outToServer.println(str); if ("exit" .equals(str)) { flag = false ; } else if ("shutdownServer" .equals(str)) { flag = false ; new Socket ("localhost" , 6666 ); } else { String echo = inputByServer.readLine(); System.out.println(echo); } } inputByServer.close(); client.close(); } }
运行效果 打开两个客户端,并各自发送两个字符串,显示效果如下图: 关闭一个客户端,然后再打开,则此时服务线程的数量先减一后加一,服务线程(等于客户端数量)数量不变. 现在输入shutdownServer关闭服务器,效果如下: 这只对新的客户端有影响,对已经连接的客户端并没有影响. 当所有连接过的客户端都结束后,服务端没有任何线程在运行,此时服务器才真正的结束: