JAVA实现客户端与服务器端的TCP通信
(JAVA 工程训练阶段一、训练任务三基本通信能力、基本任务3.2javaTCP 通信)
编写两个java application 应用程序,完成以下功能:
(1)一个程序为服务端,建立TCP 服务端套接字。
(2)另外一个程序为客户端,建立TCP 客户端套接字。
这两个程序可以互联,完成一个基于TCP/IP 网络的文本聊天程序。
TCP 通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket 对象,从而在通信的两端形成网络虚拟链路,一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信Java 对基于TCP 协议的的网络提供了良好的封装,使用Socket 对象来代表两端的通信端口,并通过Socket 产生IO 流来进行网络通信Java 为客户端提供了Socket 类,为服务器端提供了ServerSocket 类。
发送数据的步骤: 创建客户端的Socket 对象(Socket)Socket(String host, int port);获取输出流,写数据OutputStream getOutputStream(); 释放资源void close()。
接收数据的步骤:创建服务器端的Socket 对象(ServerSocket) ServerSocket(int port);监听客户端连接,返回一个Socket 对象Socket accept();获取输入流,读数据,并把数据显示在控制台InputStream getInputStream();释放资源void close()。
TCPClient.java 功能:用户端的通信线程聊天室,客户端封装了TCP 通讯协议,使用它与远程计算机进行网络通讯实例化Socket 需要传入两个参数:服务端ip 地址、服务端端口(0-65535),通过IP 地址可以找到网络上的服务端所在的计算机,通过端口可以连接到该计算机上的服务端,应用程序实例化Socket 的过程是建立连接的过程,所以若连接服务端失败,会抛出异常。代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;//无法确定主机IP地址时抛出异常
import java.util.Scanner;
/**
* 聊天室客户端
*
*/
public class TCPClient {
/*
* java.net.Socket
* 封装了TCP通讯协议,使用它与远程计算机进行网络通讯
*/
private Socket socket;//实现客户端套接字,实际工作是由Socketlmpl类的一个实例执行的
/**
* 构造方法,用来初始化客户端
* @throws IOException
* @throws UnknownHostException
*/
public TCPClient() throws UnknownHostException, IOException{
/*
* 实例化Socket是需要传入两个参数
* 1:服务端IP地址
* 2:服务端端口(0~65535)
* 通过IP地址可以找到网络上的服务端所在的计算机
* 通过端口可以连接到该计算机上的服务端应用程序
* 实例化Socket的过程就是建立连接的过程,所以若连接服务端失败,这里会抛出异常。
*/
System.out.println("正在与服务端建立连接...");
socket = new Socket("localhost",8088);//创建一个流套接字,并将其连接到localhost上的8088端口。
System.out.println("与服务端连接成功");
}
/**
* 客户端的启动方法,从这里开始执行客户端逻辑
*/
public void start() {
try {
Scanner sc = new Scanner(System.in);
OutputStream out = socket.getOutputStream();//返回此套接字的输出流给out,OutputStream将操作委托给通道套接字
OutputStreamWriter osw = new OutputStreamWriter(out,"utf-8");//从字符流out中写入多个字符转换成的字节流到osw,使用指定的名称为UTF-8的字符集,也可以使用平台默认的字符集
PrintWriter pw = new PrintWriter(osw,true);//将osw字节流格式化表示输出到文本输出流pw
ServerHandler handler = new ServerHandler();//接收客户端消息输出到客户端控制台的线程
Thread t = new Thread(handler);//创建新的线程
t.start();//启动读取服务端发送过来消息的线程
System.out.println("开始聊天吧!");
String line = null;
while(true){
line = sc.nextLine();//此方法返回当前行的其余部分,不包括末尾的任何行解析器。位置设置为下一行的开头。
System.out.println("你说:"+line);
pw.println(line);//文本输出
}
} catch (Exception e) {
}
}
public static void main(String[] args) {
try {
TCPClient client = new TCPClient();//使用初始化的TCPClient()方法
client.start();//客户端启用方法
} catch (Exception e) {
e.printStackTrace();//错误回溯
System.out.println("客户端启动失败!");
}
}
/**
* 该线程用来循环接收服务端发送过来的消息并输出到客户端自己的控制台上
*/
private class ServerHandler implements Runnable{
public void run() {
try {
InputStream in = socket.getInputStream();//返回套接字输入字节流给in,InputStream将操作委托给通道套接字
InputStreamReader isr = new InputStreamReader(in,"UTF-8");//从字节流in中读取多个字节转换为字符流到isr,使用指定的名称为UTF-8的字符集,也可以使用平台默认的字符集
BufferedReader br = new BufferedReader(isr);//从字符输入流isr中读取文本到br,缓冲字符提供对字符、数组、和行的有效读取。
String message = null;
while((message = br.readLine())!=null){//从字符文本集中获取消息文本
System.out.println("[服务端]说:"+message);
}
} catch (Exception e) {
}
}
}
}
TCPServer.java 程序及其功能:聊天室服务端:运行在服务端的ServerSocket 可实现申请服务端口、监听服务端口,一旦一个客户端通过该端口建立连接,则创建一个Socket 用于与该客户端通讯,存放的客户端的输出流,用于将消息广播给所有客户端,初始化ServerSocket 指定的服务端口不能与系统其它应用程序已申请的端口号重复,否则会抛出异常,服务端会监听ServerSocket 申请的服务端口,通过阻塞方法,直到一个客户端通过该端口连接才会返回一个用于与连接的客户端进行通讯的Socket,之后服务器启动一个线程与该客户端交互,将给定的消息广播给所有客户端、转发给所有客户端。Socket 可以获取一个输入流,通过该流客户读取到远端计算机发送过来的数据,通过Socket 获取输出流,用于将数据发送给客户端处理客户端断开连接以后的工作最后将该客户端的输出流从共享集合中删除。代码如下:
mport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
*
聊天室服务端
*
*/
public class TCPServer {
/*
* java.net.ServerSocket
* 运行在服务端的ServerSocket主要有两个作用
* 1:申请服务端口
* 2:监听服务端口,一旦一个客户端通过该端口建立连接,则创建一个Socket用于与该客户端通讯
*/
private ServerSocket server;//ServerSocket类实现服务器套接字
//private ServerSocket server2;
/*
* 该集合用来存放所有客户端的输出流,用于将消息广播给所有客户端
*/
private List<PrintWriter> allOut;//创建列表
public TCPServer() throws IOException{
/*
* 初始化ServerSocket的同时需要指定服务端口
* 该端口号不能与系统其它应用程序已申请的端口号重复,否则会抛出异常。
*/
server = new ServerSocket(8088);//创建一个服务器套接字,绑定到8088端口。端口号为0时表示端口号是自动分配的
//server = new ServerSocket(8089);
allOut = new ArrayList<PrintWriter>();//构造初始容量为10的PrintWriter文本输出流空列表
}
public void start(){
Socket socket=null;
try {
/*
* ServerSocket提供方法:
* Socket accept()
* 该方法会监听ServerSocket申请的服务端口。 这是一个阻塞方法,直到一个客户端通过该端口连接, 才会返回一个Socket。这个返回的Socket是用于与连接的客户端进行通讯的
*/
while(true){
System.out.println("等待客户端连接...");
socket = server.accept();//监听到这个套接字的连接并接受。该方法将阻塞,直到建立连接为止。
System.out.println("一个客户端连接了!");
/*
* 启动一个线程 与该客户端交互
*/
Thread t = new Thread(new ClientHandler(socket));//定义一个线程t执行类ClientHandler定义的不含参数的run实例与指定socket的客户端互交
t.start();
}
} catch (Exception e) {
e.printStackTrace();
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
/**
* 将给定的消息广播给所有客户端
*
*/
private void sendMessage(String message){
synchronized(allOut){
//列表同步,转发给所有客户端
for(PrintWriter o: allOut){//对列表的文本输出流o
o.println(message);
}
}
}
public static void main(String[] args) {
try {
TCPServer server = new TCPServer();//聊天室服务端
server.start();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*与指定客户端互交
*
*/
private class ClientHandler implements Runnable{//Runnable接口通过类ClientHandler实现,这个类ClientHandler定义了不含参数的run实例方法通过线程t来执行。
/*
*当前线程通过这个Socket与指定客户端交互
*/
private Socket socket;
/*
* 远程计算机地址信息,这里是客户端的地址
*/
private String host ;
public ClientHandler(Socket socket){
this.socket = socket;
/*
*通过Socket可以获取远端计算机地址信息
*/
InetAddress address = socket.getInetAddress();
/*
* 获取远端计算机IP地址的字符串格式
*/
host = address.getHostAddress();
}
/**
* 客户端读取远端数据,类ClientHandler定义的不含参数的run实例方法
*
*/
public void run() {
PrintWriter pw = null;
try {
/*
* InputStream getInputStream()
* Socket提供的该方法可以获取一个输入流
* 通过该流客户读取到远端计算机发送过来的数据
*/
Scanner sc = new Scanner(System.in);
InputStream in = socket.getInputStream();//返回套接字输入字节流给in,InputStream将操作委托给通道套接字
InputStreamReader isr = new InputStreamReader(in,"UTF-8");//从字节流in中读取多个字节转换为字符流到isr,使用指定的名称为UTF-8的字符集,也可以使用平台默认的字符集
final BufferedReader br = new BufferedReader(isr);//从字符输入流isr中读取文本到br,缓冲字符提供对字符、数组、和行的有效读取。
/*
* 通过Socket获取输出流,用于将数据发送给客户端
*/
OutputStream out = socket.getOutputStream();//返回此套接字的输出流给out,OutputStream将操作委托给通道套接字
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//从字符流out中写入多个字符转换成的字节流到osw,使用指定的名称为UTF-8的字符集,也可以使用平台默认的字符集
pw = new PrintWriter(osw,true);//将格式化后的字节流osw表示输出到文本输出流pw
System.out.println("请输入系统消息:");
/*
* 将该客户端的输出流存入到共享集合中
* 由于多个线程都会调用该集合的add方法向其中添加输出流
* ,所以为了保证线程安全,可以将该集合加锁。
*/
synchronized(allOut){
allOut.add(pw);
}
String outMsg = null;
sendMessage(host+"上线了!当前在线人数为"+allOut.size()+"人");
while(true){
//System.out.println(host+"说:"+message);
//回复给当前客户端
Thread t2 = new Thread(new Runnable(){//定义接口Runnable()的线程t2为类ClientHandler定义的不含参数的run实例与指定socket的客户端互交
public void run() {
String message = null;
try {
while((message = br.readLine())!=null){ //从文本br读取消息
System.out.println("["+host+"]"+"说:"+message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
);
t2.start();
outMsg = sc.nextLine();
pw.println(outMsg);
System.out.println("你(服务器)说:"+outMsg);
//sendMessage(host+"说"+message);
}
} catch (Exception e) {
}finally{
//处理客户端断开连接以后的工作
//将该客户端的输出流从共享集合中删除
synchronized(allOut){
allOut.remove(pw);
}
sendMessage(host+"下线了");
if(socket!=null){
try {
socket.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
}
}
}
启动两个windows 命令行窗口,编译TCPClient.java 与TCPServer.java,实现基于TCP/IP网络的文本交互聊天程序,运行结果如图: