Java网络编程之TCP/IP
1.1 TCP/IP协议介绍 TCP/IP是Transmission Control Protocol/Internet Protocol的简写,中文译为传输控制协议/因特网互联协议,又叫做网络通讯协议,由网络层的IP协议和传输层的TCP协议组成,是Internet最基本的协议、Internet国际互联网络的基础,其主要协议如图
java TCP/IP编程
服务器端:
public class Server { private ServerSocket server; /** * 构造方法,用来初始化参数 */ public Server(){ try { /* * 向系统申请2222端口, * 外界可以通过网络发送过来的数据 */ server=new ServerSocket(2222); } catch (Exception e) { e.printStackTrace(); } } /** * 启动服务器 */ public void start(){ try { System.out.println("info: 等待客户端链接......"); Socket socket=server.accept();//io堵塞 System.out.println("info: 客户端"+socket.getLocalAddress().toString()+" 链接上服务器......"); /** * 获取客户端的输出流,用于获取从客户端 */ BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8")); String message=null; while((message=in.readLine())!=null){ System.out.println("客户端 :"+message); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Server server=new Server(); server.start(); }}
server=new ServerSocket(2222);//服务器端,监听2222端口,Socket socket=server.accept();//io堵塞 ,等待接受客户端的请求,返回的是客户端的Socket对象,通过Socket对象实现双向通信
客户端:
package com.jsd.jsd1408.chat;import java.io.OutputStreamWriter;import java.io.PrintWriter;import java.net.Socket;import java.util.Scanner;/** * @Description: 网络编程案例-客户端 * @author king-pan * @date 2014年10月9日 * @version V1.0 *//* * socket 通常称作"套接字",用来描述ip地址和端口,是一个通信链的句柄 * * 在internet上的主机一般运行了多个服务软件,同时提供几种服务。 * * 每种服务都打开一个socket,并绑定到一个端口上, * * 不同的端口对应于不同的服务。 */public class Client { /* * 客户端运行的socket,用于链接服务器 */ private Socket socket; /** * 启动客户端 */ public Client(){ try { socket=new Socket("localhost", 2222); } catch (Exception e) { e.printStackTrace(); } } /** * 启动客户端 */ public void start(){ try { /** * 在客户端,若我们希望向服务器发送数据 * 我们就通过客户端的Socket获取输出流 * 然后向输出流写出数据就行了 */ //字节流太麻烦 OutputStreamWriter osw=new OutputStreamWriter(socket.getOutputStream(),"UTF-8"); PrintWriter out=new PrintWriter(osw,false); Scanner scanner=new Scanner(System.in); String message=null; while((message=scanner.nextLine())!=null){ out.print(message+"\r\n"); out.flush(); System.out.println("客户端: 发送消息成功"); } out.close(); scanner.close(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Client client=new Client(); client.start(); }}
socket=new Socket("localhost", 2222);//连接指定ip:port 的服务器然后通过socket的输出,输入流实现双向通信
改进代码:
服务器端:
package com.jsd.jsd1408.chat;import java.io.BufferedReader;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.io.OutputStreamWriter;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ServerForm { private ServerSocket server; /* * 连接池,用来控制和管理与客户端交互的线程 */ private ExecutorService threadPool; /* * 该集合用来保存当前服务器中所有客户端的输出流 用来实现广播效果。 */ private ListallOut; /** * 构造方法,用来初始化参数 */ public ServerForm() { try { /* * 初始化用来存放所有客户端输出流的集合 */ allOut = new ArrayList (); allOut = Collections.synchronizedList(allOut); /* * 向系统申请2222端口, 外界可以通过网络发送过来的数据 */ server = new ServerSocket(2222); /* * 初始化线程池 */ threadPool = Executors.newFixedThreadPool(50); } catch (Exception e) { e.printStackTrace(); } } /* * 下面三个方法: * 1:向集合中添加输出流 * 2:从集合中删除输出流 * 3:遍历集合 * 这三个操作既要做到分别同步,又要做到三者互斥 * 这是因为多线程需要对这个集合做这三个操作。对于 * 线程安全的集合而言,自身是可以做到add,remove * 互斥的。并且保证方法同步。但是无法做到与遍历进行 * 互斥。所以我们需要自己来维护三个操作的互斥关系。 */ /** * 将给定的输出流存入共享集合allOut * * @param pw */ public synchronized void addOut(PrintWriter pw) { // 将给定的流存入集合 allOut.add(pw); } /** * 将给定的输出流从共享集合中删除 * * @param pw */ public synchronized void removeOut(PrintWriter pw) { // 将给定的流从集合中删除 allOut.remove(pw); } /** * 将给定的消息发送给每一个客户端 * * @param message */ public synchronized void sendMessageToAllClient(String message) { /* * 遍历集合,将给定的字符串发送给每一个输出流 从而达到广播消息的效果 */ for (PrintWriter pw : allOut) { pw.println(message); } } /** * 启动服务器 */ public void start() { try { while (true) { System.out.println("info: 等待客户端链接......"); Socket socket = server.accept();// io堵塞 System.out .println("info: 客户端" + socket.getLocalAddress().toString() + " 链接上服务器......"); /* * 当一个客户端链接后,我们启动一个线程, 并将该客户端的Socket传入到线程里,使得 * 让该线程来完成与当前客户端的交互工作。 这样我们就可以再次调用accept方法等待 其他客户端的链接了。 */ GetClientHandler handler = new GetClientHandler(socket); // Thread t = new Thread(handler); // t.start(); /* * 将任务交给线程池 */ threadPool.execute(handler); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(); server.start(); } /** * 该线程体的作用是针对给定的客户端进行交互操作 实际工作用于接受客户端的消息并做处理 * * @author Administrator * */ class GetClientHandler implements Runnable { /* * 该线程用于交互的客户端的Socket */ private Socket socket; public GetClientHandler(Socket socket) { this.socket = socket; } public void run() { PrintWriter pw = null; try { /* * 通过Socket获取输出流,用于将消息 发送给客户端 */ OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8"); pw = new PrintWriter(osw, true); /* * 将当前线程交互的客户端的输出流存入 共享集合,以便于可以广播消息给当前 客户端 */ addOut(pw); /* * 通过Socket获取输入流 然后将输入流转换为缓冲字符输入流 循环读取该客户端发送过来的消息 */ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); String message = null; /* * 这里判断读取的内容是不是null是出于对linux 与windows底层实现细节的差异 当客户端与服务端断开链接后 * 若客户端是linux用户,那么服务端的br.readLine() 方法会返回null,返回null对于缓冲流而言也是表示再 * 没有消息可以读取了。所以就应当停止读取工作了。 但是windows的客户端若断开链接,则br.readLine() * 方法会直接抛出异常。 */ while ((message = br.readLine()) != null) { // System.out.println("客户端说:"+message); // 将消息发送至客户端 // pw.println(message); /* * 当该客户端发送一条消息过来后,我们将该 消息转发给所有客户端,达到广播的效果 */ sendMessageToAllClient(message); } } catch (Exception e) { e.printStackTrace(); } finally { try { /* * 当客户端与服务端断开连接后,先将 该客户端的输出流从共享集合中删除 停止对该客户端广播消息 */ removeOut(pw); /* * 当客户端与服务端断开链接后 服务端这边也应当将Socket关闭 以释放资源 */ if (socket != null) { socket.close(); } } catch (Exception e2) { e2.printStackTrace(); } } } }}
服务器端,循环接受客户端请求,把每个客户端请求交给一个线程,然后让线程去负责该客户端。
客户端:
package com.jsd.jsd1408.chat;import java.io.BufferedReader;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.util.Scanner;/** * @Description: 网络编程案例-客户端 * @author king-pan * @date 2014年10月9日 * @version V1.0 *//* * socket 通常称作"套接字",用来描述ip地址和端口,是一个通信链的句柄 * * 在internet上的主机一般运行了多个服务软件,同时提供几种服务。 * * 每种服务都打开一个socket,并绑定到一个端口上, * * 不同的端口对应于不同的服务。 */public class ClientForm { /* * 客户端运行的socket,用于链接服务器 */ private Socket socket; /** * 启动客户端 */ public ClientForm() { try { socket = new Socket("localhost", 2222); } catch (Exception e) { e.printStackTrace(); } } /** * 客户端开始工作的方法 */ public void start() { try { /* * 启动客户端中用来读取服务端发送消息的线程 */ GetServerMessageHandler handler = new GetServerMessageHandler(); Thread t = new Thread(handler); t.start(); /* * 在客户端,若我们希望向服务端发送数据 我们就通过客户端这边的Socket获取 输出流,然后向输出流写出数据就可以了 */ OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8"); PrintWriter pw = new PrintWriter(osw, true); // 创建一个Scanner用于获取用户输入 Scanner scanner = new Scanner(System.in); System.out.println("信息提示 : 请输入聊天昵称 ......"); pw.println(scanner.nextLine()); /* * 将字符串发送至服务端 */ while (true) { pw.println(scanner.nextLine()); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { ClientForm client = new ClientForm(); client.start(); } /** * 该线程的作用是用于接收服务端发送过来的信息 并输出到客户端的控制台上 * * @author Administrator * */ class GetServerMessageHandler implements Runnable { public void run() { try { /* * 1.通过Socket获取输入流 2.转换为缓冲字符输入流 3.循环读取服务端发送的消息并输出到 控制台 */ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); String message = null; while ((message = br.readLine()) != null) { System.out.println(message); } } catch (Exception e) { e.printStackTrace(); } } }}
客户端连接上服务器,然后与服务器交互
需要注意的是,使用PrintWrite 的println(),print(),write()方法,print(),write(); 需要在输出的内容后面加上"\r\n"表示该行结束。