Java开发揭开socket编程的面纱


什么是socket?
网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。socket另外也支持UDP报文协议的传输。

在学习socket编程之前先弄明白两个协议:TCP/IP协议和UDP协议。
TCPTransmission Control Protocol 传输控制协议,TCP是一种面向连接(连接导向)的、可靠的、基于字节流的运输层(Transport layer)通信协议。
UDPUser Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。另外UDP传输数据时是大小限制的,每个被传输的数据报必须限定在64KB之内

说起TCP/IP我们不得已又要想到经典的三次握手:
第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认。
第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态。

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。


对于细说TCP/IP协议的三次握手和四次挥手可移步到:http://blog.csdn.net/li0978/article/details/52598121

Socket在OSI网络七层协议上的位置:


上边也指出了一个socket是由一个IP和一个端口号来确定的,那么什么是IP?什么是端口呢?
IP地址
每台联网的电脑都有一个唯一的IP地址,标着这台电脑在网络上的位置。
IP分为IPV4和IPV6。我们常用的IPV4长度是32位的,分为4段,每段8位,十进制数字表示,每段数字的范围是0~255。IPV6长度是128位,分为8段,每段16位。
特殊IP地址:
127.0.0.1
指的是本机,主要作用是预留下作为测试使用,用于网络软件测试以及本地机进程间通信。在Windows系统下,该地址还有一个别名叫 “localhost”。

10.*.*.*

172.16.*.*――172.31.*.*

192.168.*.*

上面三个网段是私有地址,可以用于自己组网使用,这些地址主要用于企业内部网络中,但不能够在Internet网上使用,Internet网没有这些地址的路由,而使用这三个网段的计算机要上网必须要通过地址翻译(NAT),将私有地址翻译成公用合法的IP地址。

0.0.0.0

所有不清楚的主机和目的网络都用此来代替,代表这类情况的一个集合,严格意义上来说,0.0.0.0已经不是真正意义上的ip地址了。
255.255.255.255
有限广播地址,在主机不知道本机所处的网络时(如主机的启动过程中),只能采用有限广播方式,通常由无盘工作站启动时使用,希望从网络IP地址服务器处获得一个IP地址。 当广播地址包含一个有效的网络号和主机号,技术上就称为直接广播地址。

169.254.*.*

如果你的主机使用了DHCP功能自动获得一个ip地址,那么当你的DHCP服务器发生故障或响应时间太长而超出系统规定的一个时间,windows系统会为你分配这样一个地址。如果你发现你的主机ip地址是个诸如此类的地址,很不幸,十有八九是你的网络不能正常运行了。
端口
在网络上有很多电脑,这些电脑一般运行了多个网络程序。每种网络程序都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的网络程序。
端口常规分三种形式:
1.公认端口(WellKnownPorts):从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议。例如:80端口实际上总是HTTP通讯。 
2.注册端口(RegisteredPorts):从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。 
3.动态和/或私有端口(Dynamicand/orPrivatePorts):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。
所以我们常常设置端口的时候最好设成1023之后的。
常用端口:21( FTP) ,25 (SMTP) ,110 (POP3) ,80(HTTP), 443(HTTPS)

有了以上的基础,对于socket编程的学习即可事半功倍了,socket编程的具体流程如下:

下面我以用户登录案例来阐述socket通信的这一过程,上边也指出socket通信不仅仅根据TCP/IP协议来进行数据传输还可以根据UDP协议数据传输。根据上图我们知道服务器至少要有两个socket才能与客户端进行三次握手,第一个的socket叫ServerSocket,用来监听等待客户端的链接,第二个socket就是常规的socket,用于对客户端数据的接受和反馈。
根据TCP/IP协议进行socket通信模拟用户登录
服务器端:
/**
 * 服务器端
 */
public class Server {
	public static void main(String[] args) {
		try {
			//1.创建一个服务器端socket即ServerSocket,指定绑定的端口,并监听此端口
			ServerSocket  serverSocket = new ServerSocket(8888);
			System.out.println("+++++++++++++++++服务器即将启动,等待客户端的链接+++++++++++++");
			//2.调用accept()方法开始监听,等待客户端的连接。
			Socket socket = serverSocket.accept();
			
			//3.获取输入流,并读取客户端信息
			InputStream is = socket.getInputStream();   //获取字节输入流
			InputStreamReader isr = new InputStreamReader(is); //将字节输入流转换成字符流
			BufferedReader br = new BufferedReader(isr); //为输入流添加缓冲
			String info = null;
			while((info = br.readLine())!=null){   //循环读取客户端的信息
				System.out.println("我是服务器,客户端说:"+info);
			}
			socket.shutdownInput();  //关闭输入流
			
			//4.获取输出流,响应客户端的请求
			OutputStream os = socket.getOutputStream();
			PrintWriter pw = new PrintWriter(os);
			pw.write("谢谢你告诉我账户和密码。");
			pw.flush();
			socket.shutdownOutput();
			
			//5.关闭资源
			pw.close();
			os.close();
			br.close();
			isr.close();
			is.close();
			socket.close();
			serverSocket.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
客户端:
/**
 * 客户端
 */
public class Client {
	public static void main(String[] args) {
		try {
			// 1.创建客户端Socket,指定服务器端的IP地址和端口号。
			Socket socket = new Socket("127.0.0.1", 8888);
			
			// 2.获取输出流,向服务器端发送信息。
			OutputStream os = socket.getOutputStream(); // 字节输出流
			PrintWriter pw = new PrintWriter(os); // 将输出流包装成打印流
			pw.write("账户是admin;密码是123。");
			pw.flush();
			socket.shutdownOutput(); // 关闭输出流
			
			//3.获取输入流,读取服务器的响应信息。
			InputStream is = socket.getInputStream();
			InputStreamReader isr = new InputStreamReader(is);
			BufferedReader br = new BufferedReader(isr);
			String info = null;
			while ((info = br.readLine())!=null) {
				System.out.println("我是客户端,服务器说:"+info);
			}
			socket.shutdownInput();
			
			// 4.关闭资源
			br.close();
			isr.close();
			is.close();
			pw.close();
			os.close();
			socket.close();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}
运行时先开启服务端在开启客户端,运行结果如下:
服务端显示:


客户端显示:


多线程多客户端模拟用户登陆
实际应用中不会这么简单的一个服务器对应一个客户端,而最常见的是一个服务器形成一个服务中心多个客户端进行连接,这就要用到多线程机制了。也就是在服务器端开启多个线程,每个线程去去单独应对每一个连接的客户端。
很容易想到启动多少个客户端咱们就启动多少个线程,而ServerSocket一次只能监听一个客户端的连接,所以这里要创建一个死循环让其不间断的一个一个的去监听。又因为处于死循环状态,服务器一直处于工作状态不能自主关闭,所以也就不能调用serverScoket.close()了。

服务器端子线程类:
用于对每个连接的客户端进行通信。
/**
 * 服务器端子线程
 */
public class ServerThread extends Thread {
	private Socket socket = null;  //拿到连接客户端的socket

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

	@Override
	public void run() {
		super.run();
		InputStream is = null;
		InputStreamReader isr = null;
		BufferedReader br = null;
		OutputStream os = null;
		PrintWriter pw = null;
		try {
			//获取输入流,并读取客户端信息
			is = socket.getInputStream(); // 获取字节输入流
			isr = new InputStreamReader(is); // 将字节输入流转换成字符流
			br = new BufferedReader(isr); // 为输入流添加缓冲
			String info = null;
			while ((info = br.readLine()) != null) { // 循环读取客户端的信息
				System.out.println("我是服务器,客户端说:" + info);
			}
			socket.shutdownInput(); // 关闭输入流

			//获取输出流,响应客户端的请求
			os = socket.getOutputStream();
			pw = new PrintWriter(os);
			pw.write("谢谢你告诉我账户和密码。");
			pw.flush();
			socket.shutdownOutput();
			
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			//关闭资源
			try {
				if (pw != null)
					pw.close();
				if (os != null)
					os.close();
				if (br != null)
					br.close();
				if (isr != null)
					isr.close();
				if (is != null)
					is.close();
				if (socket != null)
					socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
服务器端:
/**
 * 服务器端
 */
public class Server {
	public static void main(String[] args) {
		try {
			//创建一个服务器端socket即ServerSocket,指定绑定的端口,并监听此端口
			ServerSocket  serverSocket = new ServerSocket(8888);
			Socket socket = null;
			//记录客户端的数量
			int count = 0;
			System.out.println("+++++++++++++++++服务器即将启动,等待客户端的链接+++++++++++++");
			//创建一个死循环,循环监听等待客户端的链接。
			while(true){
				//调用accept()方法开始监听,等待客户端的连接。
				socket = serverSocket.accept();
				//创建一个服务器线程
				ServerThread serverThread = new ServerThread(socket);
				//启动线程
				serverThread.start();
				
				count++; //统计客户端的数量
				System.out.println("当前客户端连接的数量:"+count);
			}
			
			//serverSocket.close(); //因为创建了死循环所以ServerSocket一直处于工作状态,所以ServerSocket也就无法关闭了。
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
客户端:
和上边的客户端一样。

运行时还是先开启服务器端在开启客户端,并开启多个客户端,控制台查看连接情况:


根据UDP协议进行socket通信模拟用户登录
使用UDP协议进行数据传输时,首先需要将要传输的数据定义成数据报(Datagram),在数据报中指明数据所要达到的Socket(主机地址和端口号),然后再将数据报发送出去。这里的端口连接和数据的接受发送不在是上边TCP形式的ServerSocket和Socket了,这里要使用到的有两个类:
DatagramSocket:进行端到端通信的类,用于服务器端创建端口和接收数据包和客服端发送数据包。
DatagramPacket:表示数据报包,是UDP通信的数据单元。
下面看一下具体过程:
服务器端:
/**
 * UDP形式的服务器端
 */
public class UDPServer {
	public static void main(String[] args) throws IOException {
		//创建服务器端DatagramSocket,指定端口
		DatagramSocket datagramSocket = new DatagramSocket(8888);
		System.out.println("+++++++++UDP++++++++服务器即将启动,等待客户端的链接+++++++++++++");
		
		/*
		 * 服务器接收客户端的发送来的数据
		 */
		//创建数据报,用于接受客户端发送来的数据。
		byte[] data = new byte[1024];
		DatagramPacket packet = new DatagramPacket(data, data.length);
		//接收客户端发送来的数据
		datagramSocket.receive(packet);//在接收数据前此方法一直处于阻塞状态。
		//读取客户端发送来的数据信息
		String info = new String(data, 0, packet.getLength());
		System.out.println("我是服务器,客户端说:" + info);
		
		/*
		 * 服务器响应客户端的请求
		 */
		//定义客户端的地址,端口,发送的数据。
		InetAddress address = packet.getAddress();
		int port = packet.getPort();
		byte[] data2 = "谢谢你告诉我账号和密码".getBytes();
		//创建数据报,封装响应信息
		DatagramPacket packet2 = new DatagramPacket(data2, 0, data2.length,address, port);
		//响应客户端
		datagramSocket.send(packet2);
		
		//关闭socket
		datagramSocket.close();
	}
}

客户端:
/**
 * UDP形式的客户端
 */
public class UDPClient {
	public static void main(String[] args) throws IOException {
		//创建客户端DatagramSocket,用于将来向服务器端发送数据报和接收服务器端的响应数据报
		DatagramSocket datagramSocket = new DatagramSocket();
		
		/*
		 * 向服务器端发送数据
		 */
		//定义服务器的地址,端口号,数据。
		InetAddress address = InetAddress.getByName("127.0.0.1");
		int port = 8888;
		byte[] data = "账户是admin,密码是123".getBytes();
		//创建数据报,封装发送的数据信息。
		DatagramPacket packet = new DatagramPacket(data, 0, data.length,address, port);
		//发送数据
		datagramSocket.send(packet);
		
		/*
		 * 接收服务器端响应数据
		 */
		//创建数据报,用于接收服务器端的响应数据。
		byte[] data2 = new byte[1024];
		DatagramPacket packet2 = new DatagramPacket(data2, data2.length);
		//接收服务器端响应的数据
		datagramSocket.receive(packet2);
		//读取服务器端响应的数据
		String info = new String(data2, 0, packet2.getLength());
		System.out.println("我是客户端,服务器说:" + info);
		
		//关闭socket
		datagramSocket.close();
	}
}
运行时还是先开启服务端再开启客户端,运行结果如下:
服务端显示:

客户端显示:

到此为止相信对socket通信有一定了解了吧,这里仍然需要说明一下:
在多线程模拟用户登录时由于创建了一个死循环去不断的监听客户端的链接情况,这里建议创建子线程的时候设置一个合适的优先级,serverThread.setPriority(4)。适当的降低线程优先级有助于提升运行时速度。





猜你喜欢

转载自blog.csdn.net/li0978/article/details/52357753