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关闭服务器,效果如下:
这只对新的客户端有影响,对已经连接的客户端并没有影响.
当所有连接过的客户端都结束后,服务端没有任何线程在运行,此时服务器才真正的结束: