JAVA进阶——Socket编程

144 阅读14分钟

一、Socket知识

  1. Socket概述 (1)Java最初是作为网络编程语言出现的,它对网络的高度支持,使得客户端和服务器端流畅的沟通成为现实。

(2)在网络编程中,使用最多的就是Socket,每一个实用的网络程序都少不了它的参与。

(3)在计算机网络编程技术中,两个进程或者说两台计算机可以通过一个网络通信连接实现数据的交换,这种通信链路的端点就被称为“套接字”(英文名称也就是Socket)。

(4)Socket是网络驱动层提供给应用程序的一个接口或者说一种机制。

(5)使用物流送快递的例子来说明Socket:

    -->发件人将有收货人地址信息的货物送到快递站,发件人不用关心物流是如何进行的,货物被送到收货人所在地区的快递站点,进行配送,收货人等待收货就可以了。

    -->这个过程很形象地说明了信息在网络中传递的过程。其中,货物就是数据信息,2个快递站点就是2个端点Socket。

(6)信息如何在网络中寻址传递,应用程序并不用关心,只负责准备发送数据和接收数据即可。

  1. Socket通信原理 (1)对于编程人员来说,无须了解Socket底层机制是如何传送数据的,而是直接将数据提交给Socket,Socket会根据应用程序提供的相关信息,通过一系列计算,绑定IP及信息数据,将数据交给驱动程序向网络上发送。

(2)Socket的底层机制非常复杂,Java平台提供了一些简单但是强大的类,可以简单有效地使用Socket开发通信程序而无须了解底层机制。

  1. java.net包 (1)java.net包提供了若干支持基于套接字的客户端/服务器通信的类。

(2)java.net包中常用的类有Socket、ServerSocket、DatagramPacket、DatagramSocket、InetAddress、URL、URLConnection和URLEncoder等。

(3)为了监听客户端的连接请求,可以使用ServerSocket类。

(4)Socket类实现用于网络上进程间通信的套接字。

(5)DatagramSocket类使用UDP协议实现客户端和服务器套接字。

(6)DatagramPacket类使用DatagramSocket类的对象封装设置和收到的数据报。

(7)InetAddress类表示Internet地址。

(8)在创建数据报报文和Socket对象时,可以使用InetAddress类

二、基于TCP协议的Socket编程

1.Socket类和ServerSocket类 (1)java.net包的两个类Socket和ServerSocket,分别用来实现双向安全连接的客户端和服务器端,它们是基于TCP协议进行工作的,工作过程如同打电话的过程,只有双方都接通了,才能开始通话。

(2)进行网络通信时,Socket需要借助数据流来完成数据的传递工作。

(3)一个应用程序要通过网络向另一个应用程序发送数据,只要简单地创建Socket,然后将数据写入到与该Socket关联的输出流即可。对应的,接收方的应用程序创建Socket,从相关联的输入流读取数据即可。

(4)注意:2个端点在基于TCP协议的Socket编程中,经常一个作为客户端,一个作为服务器端,也就是遵循client-server模型。

● Socket类

    Socket对象在客户端和服务器之间建立连接。可用Socket类的构造方法创建套接字,并将此套接字连接至指定的主机和端口。

(1)构造方法

    -->第一种构造方法以主机名和端口号作为参数来创建一个Socket对象。创建对象时可能抛出UnknownHostException或IOException异常,必须捕获它们。

    Socket s = new Socket(hostName,port);

    -->第二种构造方法以InetAddress对象和端口号作为参数来创建一个Socket对象。构造方法可能抛出IOException或UnknownHostException异常,必须捕获并处理它们。

    Socket s = new Socket(address,port);

(2)常用方法

● ServerSocket类

    ServerSocket对象等待客户端建立连接,连接建立以后进行通信。

(1)构造方法

    -->第一种构造方法接受端口号作为参数创建ServerSocket对象,创建此对象时可能抛出IOException异常,必须捕获和处理它。

    ServerSocket ss = new ServerSocket(port);

    -->第二种构造方法接受端口号和最大队列长度作为参数,队列长度表示系统在拒绝连接前可以拥有的最大客户端连接数。

    ServerSocket ss = new ServerSocket(port,maxqu);

(2)常用方法

    -->Socket类中列出的方法也适用于ServerSocket类。

    -->ServerSocket类具有accept()方法,此方法用于等待客户端发起通信,这样Socket对象就可用于进一步的数据传输。

2.使用Socket编程实现登录功能

● 实现单用户登录

    -->Socket网络编程一般分成如下4个步骤进行:

(1)建立连接。

(2)打开Socket关联的输入/输出流。

(3)从数据流中写入信息和读取信息。

(4)关闭所有的数据流和Socket。

    -->使用两个类模拟实现用户登录的功能,实现客户端向服务器端发送用户登录信息,服务器端显示这些信息。

        客户端实现步骤:

        1)建立连接,连接指向服务器及端口。

        2)打开Socket关联的输入/输出流。

        3)向输出流中写入信息。

        4)从输入流中读取响应信息。

        5)关闭所有的数据流和Socket。

        服务器端实现步骤:

        1)建立连接,监听端口。

        2)使用accept()方法等待客户端发起通信

        3)打开Socket关联的输入/输出流。

        4)从输入流中读取请求信息。

        5)向输出流中写入信息。

        6)关闭所有的数据流和Socket。

    -->客户端和服务器端的交互,采用一问一答的模式,先启动服务器进入监听状态,等待客户端的连接请求,连接成功以后,客户端先“发言”,服务器给予“回应”。

示例01:实现传递对象信息。

♥ user类

package cn.bdqn.demo02;

import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = 1L;
/** 用户名 */
private String loginName;
/** 用户密码 */
private String pwd;

public User() {
	super();
}

public User(String loginName, String pwd) {
	super();
	this.loginName = loginName;
	this.pwd = pwd;
}

public String getLoginName() {
	return loginName;
}

public void setLoginName(String loginName) {
	this.loginName = loginName;
}

public String getPwd() {
	return pwd;
}

public void setPwd(String pwd) {
	this.pwd = pwd;
}

}

♥ LoginServer类

package cn.bdqn.demo02;

import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket;

public class LoginServer {

public static void main(String[] args) {
	ServerSocket serverSocket = null;
	Socket socket = null;
	InputStream is = null;
	ObjectInputStream ois = null;
	OutputStream os = null;
	try {
		// 建立一个服务器Socket(ServerSocket),指定端口8800并开始监听
		serverSocket = new ServerSocket(8800);
		// 使用accept()方法等待客户端发起通信
		socket = serverSocket.accept();
		// 打开输入流
		is = socket.getInputStream();
		// 反序列化
		ois = new ObjectInputStream(is);
		// 获取客户端信息,即从输入流读取信息
		User user = (User) ois.readObject();
		if (user != null) {
			System.out.println("我是服务器,客户登录信息为:" + user.getLoginName() + ","
					+ user.getPwd());
		}

		// 给客户端一个响应,即向输出流中写入信息
		String reply = "欢迎你,登录成功";
		os = socket.getOutputStream();
		os.write(reply.getBytes());
	} catch (IOException e) {
		e.printStackTrace();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} finally {
		// 关闭资源
		try {
			os.close();
			ois.close();
			is.close();
			socket.close();
			serverSocket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

}

♥ LoginClient类

package cn.bdqn.demo02;

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException;

public class LoginClient {

/*
 * 示例02:升级演示示例01,实现传递对象信息。
 */

public static void main(String[] args) {
	Socket socket = null;
	OutputStream os = null;
	ObjectOutputStream oos = null;
	InputStream is = null;
	BufferedReader br = null;
	try {
		// 建立客户端Socket连接,指定服务器的位置为本机以及端口为8800
		socket = new Socket("localhost", 8800);
		// 打开输出流
		os = socket.getOutputStream();
		// 对象序列化
		oos = new ObjectOutputStream(os);
		// 发送客户端信息,即向输出流中写入信息
		User user = new User("Tom", "123456");
		oos.writeObject(user);
		socket.shutdownOutput();

		// 接收服务器端的响应,即从输入流中读取信息
		is = socket.getInputStream();
		br = new BufferedReader(new InputStreamReader(is));
		String reply;
		while ((reply = br.readLine()) != null) {
			System.out.println("我是客户端,服务器的响应为:" + reply);
		}
	} catch (UnknownHostException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		try {
			br.close();
			is.close();
			oos.close();
			os.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

}

示例02:升级演示示例01,实现传递多个对象信息。

♥ user类

package cn.bdqn.demo03;

import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = 1L;
/** 用户名 */
private String loginName;
/** 用户密码 */
private String pwd;

public User() {
	super();
}

public User(String loginName, String pwd) {
	super();
	this.loginName = loginName;
	this.pwd = pwd;
}

public String getLoginName() {
	return loginName;
}

public void setLoginName(String loginName) {
	this.loginName = loginName;
}

public String getPwd() {
	return pwd;
}

public void setPwd(String pwd) {
	this.pwd = pwd;
}

}

♥ LoginServer类

package cn.bdqn.demo03;

import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket;

public class LoginServer {

public static void main(String[] args) {
	ServerSocket serverSocket = null;
	Socket socket = null;
	InputStream is = null;
	ObjectInputStream ois = null;
	OutputStream os = null;
	try {
		// 建立一个服务器Socket(ServerSocket),指定端口8800并开始监听
		serverSocket = new ServerSocket(8800);
		// 使用accept()方法等待客户端发起通信
		socket = serverSocket.accept();
		// 打开输入流
		is = socket.getInputStream();
		// 反序列化
		ois = new ObjectInputStream(is);
		// 获取客户端信息,即从输入流读取信息
		User[] users = (User[]) ois.readObject();
		for (int i = 0; i < users.length; i++) {
			System.out.println("我是服务器,客户登录信息为:" + users[i].getLoginName()
					+ "," + users[i].getPwd());
		}

		// 给客户端一个响应,即向输出流中写入信息
		String reply = "欢迎你,登录成功";
		os = socket.getOutputStream();
		os.write(reply.getBytes());
	} catch (IOException e) {
		e.printStackTrace();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} finally {
		// 关闭资源
		try {
			os.close();
			ois.close();
			is.close();
			socket.close();
			serverSocket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

}

♥ LoginClient类

package cn.bdqn.demo03;

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException;

public class LoginClient {

/*
 * 示例02:升级演示示例01,实现传递对象信息。
 */

public static void main(String[] args) {
	Socket socket = null;
	OutputStream os = null;
	ObjectOutputStream oos = null;
	InputStream is = null;
	BufferedReader br = null;
	try {
		// 建立客户端Socket连接,指定服务器的位置为本机以及端口为8800
		socket = new Socket("localhost", 8800);
		// 打开输出流
		os = socket.getOutputStream();
		// 对象序列化
		oos = new ObjectOutputStream(os);
		// 发送客户端信息,即向输出流中写入信息
		User user1 = new User("Tom", "123456");
		User user2 = new User("bob", "123456");
		User user3 = new User("lisa", "123456");
		User[] users = {user1,user2,user3};
		oos.writeObject(users);
		socket.shutdownOutput();

		// 接收服务器端的响应,即从输入流中读取信息
		is = socket.getInputStream();
		br = new BufferedReader(new InputStreamReader(is));
		String reply;
		while ((reply = br.readLine()) != null) {
			System.out.println("我是客户端,服务器的响应为:" + reply);
		}
	} catch (UnknownHostException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		try {
			br.close();
			is.close();
			oos.close();
			os.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

}

● 实现多客户端用户登录

     -->一问一答的模式在现实中显然不是人们想要的。一个服务器不可能只针对一个客户端服务,一般是面向很多的客户端同时提供服务的,但是单线程实现必然是这样的结果。

    -->解决这个问题的办法是采用多线程的方式,可以在服务器端创建一个专门负责监听的应用主服务程序、一个专门负责响应的线程程序。这样可以利用多线程处理多个请求。

    ->客户端实现步骤:

    1)建立连接,连接指向服务器及端口。

    2)打开Socket关联的输入/输出流。

    3)向输出流中写入信息。

    4)从输入流中读取响应信息。

    5)关闭所有的数据流和Socket。

    -->服务器端实现步骤:

    1)创建服务器线程类,run()方法中实现对一个请求的响应处理。

    2)修改服务器端代码,让服务器端Socket一直处于监听状态。

    3)服务器端每监听到一个请求,创建一个线程对象并启动。

示例03:升级演示示例02,实现多客户端的响应处理。

♥ user类

package cn.bdqn.demo04;

import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = 1L;
/** 用户名 */
private String loginName;
/** 用户密码 */
private String pwd;

public User() {
	super();
}

public User(String loginName, String pwd) {
	super();
	this.loginName = loginName;
	this.pwd = pwd;
}

public String getLoginName() {
	return loginName;
}
                                              

public String getPwd() {
	return pwd;
}

public void setPwd(String pwd) {
	this.pwd = pwd;
}

}

♥ LoginThread

package cn.bdqn.demo04;

import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.OutputStream; import java.net.Socket;

public class LoginThread extends Thread { /* * 示例03:升级示例02,实现多客户端的响应处理。 * * 分析如下: * (1)创建服务器端线程类,run()方法中实现对一个请求的响应处理。 * (2)修改服务器端代码,让服务器端Socket一直处于监听状态。 * (3)服务器端每监听到一个请求,创建一个线程对象并启动 */

Socket socket = null;
//每启动一个线程,连接对应的Socket

public LoginThread(Socket socket) {
	this.socket = socket;
}

//启动线程,即响应客户请求
public void run() {
	InputStream is = null;
	ObjectInputStream ois = null;
	OutputStream os = null;
	try {
		//打开输入流
		is = socket.getInputStream();
		//反序列化
		ois = new ObjectInputStream(is);
		//获取客户端信息,即从输入流读取信息
		User user = (User)ois.readObject();
		if(user!=null){
			System.out.println("我是服务器,客户登录信息为:"+user.getLoginName()+","+user.getPwd());
		}
		
		//给客户端一个响应,即向输出流中写入信息
		os = socket.getOutputStream();
		String reply = "欢迎你,登录成功";
		os.write(reply.getBytes());
	} catch (IOException e) {
		e.printStackTrace();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	}finally{
		try {
			os.close();
			ois.close();
			is.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}			
	}	
}	

}

♥ LoginServer类

package cn.bdqn.demo04;

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket;

public class LoginServer {

public static void main(String[] args) {
	
	ServerSocket serverSocket = null;
	try {
		// 建立一个服务器Socket(ServerSocket)指定端口并开始监听
		serverSocket = new ServerSocket(8800);

		// 监听一直进行中
		while (true) {
			// 使用accept()方法等待客户发起通信
			Socket socket = serverSocket.accept();
			LoginThread loginThread = new LoginThread(socket);
			loginThread.start();
		}
	} catch (IOException e) {
		e.printStackTrace();
	}
}

}

♥ LoginClient1类

package cn.bdqn.demo04;

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException;

public class LoginClient01 { /* * 客户端通过输出流向服务器端发送请求信息 * 服务器侦听客户端的请求得到一个Socket对象,将这个Socket对象传递给线程类 * 线程类通过输入流获取客户端的请求并通过输出流向客户端发送响应信息 * 客户端通过输入流读取服务器发送的响应信息 * */

/*
 * 示例03:升级演示示例02,实现多客户端的响应处理
 */
public static void main(String[] args) {
	
	Socket socket = null;
	OutputStream os = null;
	ObjectOutputStream oos = null;
	InputStream is = null;
	BufferedReader br = null;
	try {
		// 建立客户端Socket连接,指定服务器的位置为本机以及端口为8800
		socket = new Socket("localhost", 8800);
		// 打开输出流
		os = socket.getOutputStream();
		// 对象序列化
		oos = new ObjectOutputStream(os);
		// 发送客户端信息,即向输出流中写入信息
		User user = new User("Tom", "123456");
		oos.writeObject(user);
		socket.shutdownOutput();

		// 接收服务器端的响应,即从输入流中读取信息
		is = socket.getInputStream();
		br = new BufferedReader(new InputStreamReader(is));
		String reply;
		while ((reply = br.readLine()) != null) {
			System.out.println("我是客户端,服务器的响应为:" + reply);
		}
	} catch (UnknownHostException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		try {
			br.close();
			is.close();
			oos.close();
			os.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

}

♥ LoginClient2类和LoginClient3类

    同LoginClient1类一样,创建不同的User对象即可

    -->java.net包中的InetAddress类用于封装IP地址和DNS。要创建InetAddress类的实例,可以使用工厂方法,因为此类没有构造方法。

    -->InetAddress类中的工厂方法

    -->如果找不到主机,两种方法都将抛出UnknownHostNameException异常。

三、基于UDP协议的Socket编程 TCP

UDP

是否连接

面向连接

面向非连接

传输可靠性

可靠

不可靠

速度

1.DatagramPacket类和DatagramSocket类 (1)基于TCP的网络通信是安全的,是双向的,如同打电话,需要先有服务端,建立双向连接后,才开始数据通信。

(2)基于UDP的网络通信只需要指明对方地址,然后将数据送出去,并不会事先连接。这样的网络通信是不安全的,所以只应用在如聊天系统、咨询系统等场合下。

(3)数据报是表示通信的一种报文类型,使用数据报进行通信时无须事先建立连接,它是基于UDP协议进行的。

(4)Java中有两个可使用数据报实现通信的类,即DatagramPacket和DatagramSocket。

(5)DatagramPacket类起到容器的作用,DatagramSocket类用于发送或接收DatagramPacket。

(6)DatagramPacket类不提供发送或接收数据的方法,而DatagramSocket类提供send()方法和receive()方法,用于通过套接字发送和接收数据报。

● DatagramPacket类

(1)构造方法

    -->客户端要向外发送数据,必须首先创建一个DatagramPacket对象,再使用DatagramSocket对象发送。

(2)常用方法

● DatagramSocket类

(1)构造方法

    -->DatagramSocket类不维护连接状态,不产生输入/输出数据流,它的唯一作用就是接收和发送DatagramPacket对象封装好的数据报。

(2)常用方法

2.使用Socket编程实现客户咨询 -->利用UDP通信的两个端点是平等的,也就是说通信的两个程序关系是对等的,没有主次之分,甚至它们的代码都可以完全是一样的,这一点要与基于TCP协议的Socket编程区分开来。

    -->基于UDP协议的Socket网络编程一般按照以下4个步骤进行:

        (1)利用DatagramPacket对象封装数据包。

        (2)利用DatagramSocket对象发送数据包。

        (3)利用DatagramSocket对象接收数据包。

        (4)利用DatagramPacket对象处理数据包。



    -->模拟客户咨询功能,实现发送方发送咨询问题,接收方接收并显示发送来的咨询问题。

    发送方实现步骤:

            1)获取本地主机的InetAddress对象。

            2)创建DatagramPacket对象,封装要发送的信息。

            3)利用DatagramSocket对象将DatagramPacket对象数据发送出去。

    接收方实现步骤:

            1)创建DatagramPacket对象,准备接收封装的数据。

            2)创建DatagramSocket对象,接收数据保存于DatagramPacket对象中。

            3)利用DatagramPacket对象处理数据。

示例04:发送方发送咨询问题,接收方回应咨询。

♥ Receive类

package cn.bdqn.demo05;

import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketAddress; import java.net.SocketException;

public class Receive {

public static void main(String[] args) {
	/*
	 * 示例06:发送方发送咨询问题,接收方回应咨询。
	 * 
	 * 接收方实现步骤如下: 
	 * (1)创建DatagramPacket对象,准备接收封装的数据。
	 * (2)创建DatagramSocket对象,接收数据保存于DatagramPacket对象中。
	 * (3)利用DatagramPacket对象处理数据。
	 */

	DatagramSocket ds = null;
	DatagramPacket dp = null;
	DatagramPacket dpto = null;
	// 创建DatagramPacket对象,用来准备接收数据
	byte[] buf = new byte[1024];
	dp = new DatagramPacket(buf, 1024);
	try {
		// 创建DatagramSocket对象,接收数据
		ds = new DatagramSocket(8800);
		ds.receive(dp);
		// 显示接收到的信息
		String mess = new String(dp.getData(), 0, dp.getLength());
		System.out.println(dp.getAddress().getHostAddress() + "说:" + mess);

		String reply = "你好,我在,请咨询!";
		// 显示与本地对话框
		System.out.println("我  说:" + reply);
		// 创建DatagramPacket对象,封装数据
		SocketAddress sa = dp.getSocketAddress();
		dpto = new DatagramPacket(reply.getBytes(),
				reply.getBytes().length, sa);
		ds.send(dpto);
	} catch (SocketException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		ds.close();
	}
}

}

♥ Send类

package cn.bdqn.demo05;

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 Send { /* * 示例06:升级示例05,发送方发送咨询问题,接收方回应咨询。 * * 发送方实现步骤如下: * (1)获取本地主机的InetAddress对象。 * (2)创建DatagramPacket对象,封装要发送的信息。 * (3)利用DatagramSocket对象将DatagramPacket对象数据发送出去。 */

public static void main(String[] args) {
	DatagramSocket ds = null;
	InetAddress ia = null;
	String mess = "你好,我想咨询一个问题。";
	System.out.println("我说:" + mess);
	try {
		// 获取本地主机地址
		ia = InetAddress.getByName("localhost");
		// 创建DatagramPacket对象,封装数据
		DatagramPacket dp = new DatagramPacket(mess.getBytes(),
				mess.getBytes().length, ia, 8800);
		// 创建DatagramSocket对象,向服务器发送数据
		ds = new DatagramSocket();
		ds.send(dp);

		byte[] buf = new byte[1024];
		DatagramPacket dpre = new DatagramPacket(buf, buf.length);
		ds.receive(dpre);
		// 显示接收到的信息
		String reply = new String(dpre.getData(), 0, dpre.getLength());
		System.out.println(dpre.getAddress().getHostAddress() + "说:"
				+ reply);
	} catch (UnknownHostException e) {
		e.printStackTrace();
	} catch (SocketException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		ds.close();
	}
}

}