Java 网络 socket 编程-CSDN博客

53 阅读10分钟

使用套接字实现基于 TCP 协议的服务器和客户机程序

依据 TCP 协议,在 C/S 架构的通讯过程中,客户端和服务器的 Socket 动作如下:

客户端:

1.用服务器的 IP 地址和端口号实例化 Socket 对象。

2.调用 connect 方法,连接到服务器上。

3.将发送到服务器的 IO 流填充到 IO 对象里,比如 BufferedReader/PrintWriter。

4.利用 Socket 提供的 getInputStream 和 getOutputStream 方法,通过 IO 流对象,向服务器发送数据流。

5. 通讯完成后,关闭打开的 IO 对象和 Socket。

服务器:

1. 在服务器,用一个端口来实例化一个 ServerSocket 对象。此时,服务器就可以这个端口时刻监听从客户端发来的连接请求。

2.调用 ServerSocket 的 accept 方法,开始监听连接从端口上发来的连接请求。

3.利用 accept 方法返回的客户端的 Socket 对象,进行读写 IO 的操作通讯完成后,关闭打开的流和 Socket 对象。

 

下面是一个简单的客户端与服务器端的例子:

客户端:

package my.socket.tcp;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
/**
上述客户端代码的主要业务逻辑是:
1. 同样定义了通讯端口号,这里给出的端口号必须要和服务器端的一致。
2. 在 main 函数里,根据地址信息“localhost”,创建一个 InetAddress 类型的对象addr。这里,因为我们把客户端和服务器端的代码都放在本机运行,所以同样可以用“127.0.0.1”字符串,来创建 InetAddress 对象。
3. 根据 addr 和端口号信息,创建一个 Socket 类型对象,该对象用来同服务器端的ServerSocket 类型对象交互,共同完成 C/S 通讯流程。
4. 同样地创建 in 和 out 两类 IO 句柄,用来向服务器端发送和接收数据流。
5. 通过 out 对象,向服务器端发送"Hello Server,I am …"的字符串。发送后,同样可以用 in 句柄,接收从服务器端的消息。
6. 利用 out 对象,发送”byebye”字符串,用以告之服务器端,本次通讯结束。
7. 在 finally 从句里,关闭 Socket 对象,断开同服务器端的连接。
 * @author asus
 *
 */
public class ClientCode {
	static String clientName = "Mike";
	// 端口号
	public static int portNo = 3333;

	public static void main(String[] args) throws IOException {
		// 设置连接地址类,连接本地
		InetAddress addr = InetAddress.getByName("localhost");
		// 要对应服务器端的 3333 端口号
		Socket socket = new Socket(addr, portNo);
		try {
			System.out.println("socket = " + socket);
			// 设置 IO 句柄
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),
					true);
			out.println("Hello Server,I am " + clientName);
			String str = in.readLine();
			System.out.println(str);
			out.println("byebye");
		} finally {
			System.out.println("close the Client socket and the io.");
			socket.close();
		}
	}
}

服务器端:

package my.socket.tcp;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * 
编写服务器端的主体代码:
 这段代码的主要业务逻辑是:
1. 在上述代码里的 main 函数前,我们设置了通讯所用到的端口号,为 3333。
2. 在 main 函数里,根据给定 3333 端口号,初始化一个 ServerSocket 对象 s,该对象用来承担服务器端监听连接和提供通讯服务的功能。
3. 调用 ServerSocket 对象的 accept 方法,监听从客户端的连接请求。当完成调用accept 方法后,整段服务器端代码将回阻塞在这里,直到客户端发来 connect 请求。
4. 当客户端发来 connect 请求,或是通过构造函数直接把客户端的 Socket 对象连接到服务器端后,阻塞于此的代码将会继续运行。此时服务器端将会根据 accept 方法的执行结果,用一个 Socket 对象来描述客户端的连接句柄。
5. 创建两个名为 in 和 out 的对象,用来传输和接收通讯时的数据流。
6. 创建一个 while(true)的死循环,在这个循环里,通过 in.readLine()方法,读取从客户端发送来的 IO 流(字符串),并打印出来。如果读到的字符串是“byebye”,那么退出while 循环。
7. 在 try…catch…finally 语句段里,不论在 try 语句段里是否发生异常,并且不论这些异常的种类,finally 从句都将会被执行到。在 finally 从句里,将关闭描述客户端的连接句柄 socket 对象和 ServerSocket 类型的 s 对象。

 * @author asus
 *
 */
public class ServerCode {
	// 设置端口号
	public static int portNo = 3333;

	public static void main(String[] args) throws IOException {
		ServerSocket s = new ServerSocket(portNo);
		System.out.println("The Server is start: " + s);
		// 阻塞,直到有客户端连接
		Socket socket = s.accept();
		try {
			System.out.println("Accept the Client: " + socket);
			// 设置 IO 句柄
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),
					true);
			while (true) {
				String str = in.readLine();
				if (str.equals("byebye")) {
					break;
				}
				System.out.println("In Server reveived the info: " + str);
				out.println(str);
			}
		} finally {
			System.out.println("close the Server socket and the io.");
			socket.close();
			s.close();
		}
	}
}

先运行服务器端,再运行客户端之后,可以看到服务器端接收到来自客户端发送的信息。


通常网络编程都是用多线程来实现,将大大地提高服务器端的利用效率,并能使服务器端能具备完善的
服务功能。

package my.socket.tcp2;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
/**
 * 这个类的主要业务逻辑是:
1. 在构造函数里, 通过参数类型为 InetAddress 类型参数和 3333,初始化了本类
里的 Socket 对象,随后实例化了两类 IO 对象,并通过 start 方法,启动定义在 run 方法内的
本线程的业务逻辑。
2. 在定义线程主体动作的 run 方法里,通过 IO 句柄,向 Socket 信道上传输本客户
端的 ID 号,发送完毕后,传输”byebye”字符串,向服务器端表示本线程的通讯结束。
3. 同样地,catch 从句将处理在 try 语句里遇到的 IO 错误等异常,而在 finally 从句
里,将在通讯结束后关闭客户端的 Socket 句柄。
 * @author asus
 *
 */
class ClientThreadCode extends Thread {
	// 客户端的 socket
	private Socket socket;
	// 线程统计数,用来给线程编号
	private static int cnt = 0;
	private int clientId = cnt++;
	private BufferedReader in;
	private PrintWriter out;

	// 构造函数
	public ClientThreadCode(InetAddress addr) {
		try {
			socket = new Socket(addr, 3333);
		} catch (IOException e) {
			e.printStackTrace();
		}
		// 实例化 IO 对象
		try {
			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
			// 开启线程
			start();
		} catch (IOException e) {
			// 出现异常,关闭 socket
			try {
				socket.close();
			} catch (IOException e2) {
				e2.printStackTrace();
			}
		}
	}

	// 线程主体方法
	public void run() {
		try {
			out.println("Hello Server,My id is " + clientId);
			String str = in.readLine();
			System.out.println(str);
			out.println("byebye");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
package my.socket.tcp2;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
/**
这个类的业务逻辑说明如下:
1. 这个类通过继承 Thread 类来实现线程的功能,也就是说,在其中的 run 方法里,定义了该线程启动后要执行的业务动作。
2. 这个类提供了两种类型的重载函数。在参数类型为 Socket 的构造函数里, 通过参数,初始化了本类里的 Socket 对象,同时实例化了两类 IO 对象。在此基础上,通过 start方法,启动定义在 run 方法内的本线程的业务逻辑。
3. 在定义线程主体动作的 run 方法里,通过一个 for(;;)类型的循环,根据 IO 句柄,读取从 Socket 信道上传输过来的客户端发送的通讯信息。如果得到的信息为“byebye”,则表明本次通讯结束,退出 for 循环。
4. catch 从句将处理在 try 语句里遇到的 IO 错误等异常,而在 finally 从句里,将在通讯结束后关闭客户端的 Socket 句柄。上述的线程主体代码将会在 ThreadServer 类里被调用。
 * @author asus
 *
 */
public class ServerThreadCode extends Thread {
	// 客户端的 socket
	private Socket clientSocket;
	// IO 句柄
	private BufferedReader sin;
	private PrintWriter sout;

	// 默认的构造函数
	public ServerThreadCode() {
	}

	public ServerThreadCode(Socket s) throws IOException {
		clientSocket = s;
		// 初始化 sin 和 sout 的句柄
		sin = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
		sout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())), true);
		// 开启线程
		start();
	}

	// 线程执行的主体函数
	public void run() {
		try {
			// 用循环来监听通讯内容
			for (;;) {
				String str = sin.readLine();
				// 如果接收到的是 byebye,退出本次通讯
				if (str.equals("byebye")) {
					break;
				}
				System.out.println("In Server reveived the info: " + str);
				sout.println(str);
			}
			System.out.println("closing the server socket!");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("close the Server socket and the io.");
			try {
				clientSocket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
package my.socket.tcp2;

import java.io.IOException;
import java.net.InetAddress;
/**
 * 这段代码执行以后,在客户端将会有 3 个通讯线程,每个线程首先将先向服务器端发送"Hello
Server,My id is "的字符串,然后发送”byebye”,终止该线程的通讯。
 * @author asus
 *
 */
public class ThreadClient {
	public static void main(String[] args) throws IOException, InterruptedException {
		int threadNo = 0;
		InetAddress addr = InetAddress.getByName("localhost");
		for (threadNo = 0; threadNo < 3; threadNo++) {
			new ClientThreadCode(addr);
		}
	}
}
package my.socket.tcp2;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
这段代码的主要业务逻辑说明如下:
1. 首先定义了通讯所用的端口号,为 3333。
2. 在 main 函数里,根据端口号,创建一个 ServerSocket 类型的服务器端的 Socket,
用来同客户端通讯。
3. 在 for(;;)的循环里,调用 accept 方法,监听从客户端请求过来的 socket,请注意
这里又是一个阻塞。当客户端有请求过来时,将通过 ServerThreadCode 的构造函数,创建一
个线程类,用来接收客户端发送来的字符串。在这里我们可以再一次观察 ServerThreadCode
类,在其中,这个类通过构造函数里的 start 方法,开启 run 方法,而在 run 方法里,是通过
sin 对象来接收字符串,通过 sout 对象来输出。
4. 在 finally 从句里,关闭服务器端的 Socket,从而结束本次通讯。
 * @author asus
 *
 */
public class ThreadServer {
	// 端口号
	static final int portNo = 3333;

	public static void main(String[] args) throws IOException {
		// 服务器端的 socket
		ServerSocket s = new ServerSocket(portNo);
		System.out.println("The Server is start: " + s);
		try {
			for (;;) {
				// 阻塞,直到有客户端连接
				Socket socket = s.accept();
				// 通过构造函数,启动线程
				new ServerThreadCode(socket);
			}
		} finally {
			s.close();
		}
	}
}

首先运行服务器端,再运行客户端,可以清楚的看到服务器端多线程接收到来自客户端的消息。


下面是同时开启服务器端和客户端,两者进行不间断的通信。

package my.socket.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

public class ClientBean {
	// 描述 UDP 通讯的 DatagramSocket 对象
	private DatagramSocket ds;
	// 用来封装通讯字符串
	private byte buffer[];
	// 客户端的端口号
	private int clientport;
	// 服务器端的端口号
	private int serverport;
	// 通讯内容
	private String content;
	// 描述通讯地址
	private InetAddress ia;

	public ClientBean() throws SocketException, UnknownHostException {
		buffer = new byte[1024];
		clientport = 1985;
		serverport = 1986;
		content = "";
		ds = new DatagramSocket(clientport);
		ia = InetAddress.getByName("localhost");
	}

	public void sendToServer() throws IOException {
		buffer = content.getBytes();
		ds.send(new DatagramPacket(buffer, content.length(), ia, serverport));
	}

	// 以下是各属性的 Get 和 Set 类型方法
	public byte[] getBuffer() {
		return buffer;
	}

	public void setBuffer(byte[] buffer) {
		this.buffer = buffer;
	}

	public int getClientport() {
		return clientport;
	}

	public void setClientport(int clientport) {
		this.clientport = clientport;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public DatagramSocket getDs() {
		return ds;
	}

	public void setDs(DatagramSocket ds) {
		this.ds = ds;
	}

	public InetAddress getIa() {
		return ia;
	}

	public void setIa(InetAddress ia) {
		this.ia = ia;
	}

	public int getServerport() {
		return serverport;
	}

	public void setServerport(int serverport) {
		this.serverport = serverport;
	}
}
package my.socket.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

public class ServerBean {
	// 描述 UDP 通讯的 DatagramSocket 对象
	private DatagramSocket ds;
	// 用来封装通讯字符串
	private byte buffer[];
	// 客户端的端口号
	private int clientport;
	// 服务器端的端口号
	private int serverport;
	// 通讯内容
	private String content;
	// 描述通讯地址
	private InetAddress ia;

	public ServerBean() throws SocketException, UnknownHostException {
		buffer = new byte[1024];
		clientport = 1985;
		serverport = 1986;
		content = "";
		ds = new DatagramSocket(serverport);
		ia = InetAddress.getByName("localhost");
	}

	public void listenClient() throws IOException {
		// 在循环体里接收消息
		while (true) {
			// 初始化 DatagramPacket 类型的变量
			DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
			// 接收消息,并把消息通过 dp 参数返回
			ds.receive(dp);
			content = new String(dp.getData(), 0, dp.getLength());
			// 打印消息
			print();
		}
	}

	public void print() {
		System.out.println(content);
	}

	public DatagramSocket getDs() {
		return ds;
	}

	public void setDs(DatagramSocket ds) {
		this.ds = ds;
	}

	public byte[] getBuffer() {
		return buffer;
	}

	public void setBuffer(byte[] buffer) {
		this.buffer = buffer;
	}

	public int getClientport() {
		return clientport;
	}

	public void setClientport(int clientport) {
		this.clientport = clientport;
	}

	public int getServerport() {
		return serverport;
	}

	public void setServerport(int serverport) {
		this.serverport = serverport;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public InetAddress getIa() {
		return ia;
	}

	public void setIa(InetAddress ia) {
		this.ia = ia;
	}
	
}
package my.socket.udp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class UDPClient implements Runnable {
	public static String content;
	public static ClientBean client;

	public void run() {
		try {
			client.setContent(content);
			client.sendToServer();
		} catch (Exception ex) {
			System.err.println(ex.getMessage());
		}
	}// end of run
		// main 方法
		// …

	public static void main(String args[]) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		client = new ClientBean();
		System.out.println("客户端启动...");
		while (true) {
			// 接收用户输入
			content = br.readLine();
			// 如果是 end 或空,退出循环
			if (content == null || content.equalsIgnoreCase("end") || content.equalsIgnoreCase("")) {
				break;
			}
			// 开启新线程,发送消息
			new Thread(new UDPClient()).start();
		}
	}
}
package my.socket.udp;

import java.io.IOException;

public class UDPServer {
	public static void main(String args[]) throws IOException {
		System.out.println("服务器端启动...");
		// 初始化 ServerBean 对象
		ServerBean server = new ServerBean();
		// 开启监听程序
		server.listenClient();
	}
}

先运行服务器端,再运行客户端。

在客户端输入想要发送的字符,在服务器端可以接收到。