初探socket编程 ——JavaSocket连接与简单通信

大二学习生活开始一段时间了,最近打算整个活,尝试完成一个在线即时聊天的小程序。会更新一系列socket编程的技术文章,欢迎关注交流哦

那么千里之行,始于足下,就从这socket编程开始说起吧。

socket概念

首先一个问题,什么是socket编程?他有个中文名称叫做“套接字编程”。这个词不直观,也比较晦涩,很容易让人产生误解。我们来看一下百度百科的定义:

简介:socket一般指套接字。所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制

大致可以明白其含义是在网络上,两台主机的进程实现通信的编程技术。
但是描述还是有些不够形象,那么其实从英文原意的角度来看,socket的翻译有“插座”的意思
初探socket编程 ——JavaSocket连接与简单通信_第1张图片
这么看,将两个要相互通信的主机比作是插座和插头,发起的一方是插头,接受的一方是插座,二者的ip和端口对接上之后即可相互传输信息。这个socket可以说是上接相应的应用程序,下接通信协议栈,保证信息快速完整的传递。

这个比喻虽然不够恰当,但是也足够形象的体现socket编程的含义。

确定一个socket的标识有两个,分别是:IP端口
表示方法是点分十进制的IP地址和端口号,中间使用冒号隔开,例如:127.0.0.1:8888就是电脑本机的8888端口

Socket的工作流程

一次socket的连接与通信大致可分为以下的步骤:

  • 服务端:启动程序并开始监听连接
  • 客户端:启动程序并请求连接
  • 客户端:建立连接后像服务端发送信息
  • 服务端:建立连接并接受客户端发送的信息
  • 服务端:完成信息接受后向客户端回复信息
  • 客户端:完成信息传输后接收服务端的回复信息
  • 客户端:会话完毕,关闭socket
  • 服务端:会话完毕,关闭socket并选择关闭ServerSocket或者继续监听

当然,我们也可以选择在服务端采用多线程的形式来完成多个客户端请求的情况,从而避免服务端被一个客户端霸占,后序客户端排队的情况。这样的工作流程在刚刚的基础上可以被表示为:

  • 服务端:启动程序并开始监听连接
  • 客户端:启动程序并请求连接
  • 客户端:建立连接后像服务端发送信息
  • 服务端:确认连接并开启一个线程用来处理通信
  • 服务端:建立连接并接受客户端发送的信息
  • 服务端:完成信息接受后向客户端回复信息
  • 客户端:完成信息传输后接收服务端的回复信息
  • 客户端:会话完毕,关闭socket
  • 服务端:会话完毕,关闭socket并选择关闭ServerSocket或者继续监听

了解了大致的工作流程,下面就来了解一下socket的连接和通信

Socket连接

那么使用Java如何实现socket编程呢?

其实就是需要分别实现客户端(Client)和服务端(Server)的socket,即服务端开放“插座”等待匹配,客户端使用“插头”匹配服务端的插头。

最核心需要用到两个类,他们都在java.net包中:

  • Socket类,位于客户端(client)使用的“插头”,初始化时需要指定连接对象的IP和端口。
  • ServerSocket类,服务端(Server)的“插座”,初始化时需要制定开放的端口(服务器本机IP)

服务端需要使用ServerSocket中一个重要的方法来获取客户端的连接:accept()

这个方法可以获得客户端的socket对象,服务端使用这个对象中的io流与客户端进行通信,同样的,客户端使用socket中的io流与服务端进行通信。

上面这段描述需要注意的有两点:

  1. 客户端与服务端通过socket对象来通信,客户端创建socket,服务端使用ServerSocket对象接收并获取socket
  2. 客户端与服务端必须通过socket对象提供的io流进行通信,流中的内容会通过网络相互传输。

下面就来简单的实现一个客户端与服务端的对接:

//客户端
public class SocketClient {
     
	public static void main(String[] args) throws UnknownHostException, IOException {
     
		System.out.println("客户端程序~~~");
		System.out.println("我创建了一个socket");
		
		Socket client = new Socket("127.0.0.1",8888);
	}
}
//服务端
public class SocketServer {
     
	public static void main(String[] args) throws IOException {
     
		System.out.println("服务端程序~~~");
		System.out.println("我创建了一个ServerSocket");
		
		ServerSocket server = new ServerSocket(8888);
		
		System.out.println("开始接受socket匹配");
		
		Socket client = server.accept();
		
		System.out.println("接收到了一个socket");
	}
}

此时,运行服务端:
在这里插入图片描述
发现程序并没有执行后面的语句输出提示。这个原因在于:accept()方法在等待连接时会使程序产生阻塞,不在往下执行,直到接受到一个连接,并且返回一个客户端的Socket对象实例。

那么接着就运行客户端,让他们相互匹配:
初探socket编程 ——JavaSocket连接与简单通信_第2张图片
此时,服务端就接收到了一个socket连接,执行了后续的语句。
可以看出,在客户端,连接是在创建socket对象时就发起的,并不需要调用任何方法。
在服务端,需要使用accept方法来监听连接 ,当没有socket连接时,程序就会阻塞。

Socket通信

在建立了连接之后,服务端和客户端就要开始通信了。

上文提到过,两方的通信是通过字节流来完成的,而且这个字节流必须是socket对象提供的io流

这个io流需要通过socket的getInputStreamgetOutputStream两个方法来获取。

在使用io流进行通信的过程中,有一个需要注意的地方,就是服务端的输出流对接的是客户端的输出流,而客户端的输入流对接的是服务端的输出流。

用一张图来表示:
初探socket编程 ——JavaSocket连接与简单通信_第3张图片
其实也很符合直觉的,但是在编写程序过程中,尤其是同时编写客户端和服务端的程序时,这两个流的方向是容易搞混的,需要注意一下。

单向传递消息

现在就先从客户端向服务端传递一条消息:

/*客户端*/
public class SocketClient {
     
	public static void main(String[] args) throws UnknownHostException, IOException {
     
		System.out.println("客户端程序~~~");
		System.out.println("我创建了一个socket");
		Socket client = new Socket("127.0.0.1",8888);
		System.out.println("正在行服务端发送消息");
		/*向服务端发送消息*/
		client.getOutputStream().write("你好服务端,这里是客户端".getBytes());
		System.out.println("像服务端发送消息完毕");
		/*会话结束*/
		client.close();
	}
}
/*服务端*/
public class SocketServer {
     
	public static void main(String[] args) throws IOException {
     
		System.out.println("服务端程序~~~");
		System.out.println("我创建了一个ServerSocket");
		ServerSocket server = new ServerSocket(8888);	//创建一个服务端soscket
		System.out.println("开始接受socket匹配");
		
		Socket client = server.accept();
		
		System.out.println("接收到了一个socket");
		
		InputStream is = client.getInputStream();	//输入流
		byte[] buffer = new byte[1024];				//缓冲
		int len = 0;								//每次读取的长度(正常情况下是1024,最后一次可能不是1024,如果传输结束,返回-1)
		StringBuilder sb = new StringBuilder();		//构建读取的消息
		while((len = is.read(buffer)) != -1){
     		//接收客户端的消息
			sb.append(new String(buffer,0,len));
		}
		System.out.println("收到客户端消息:" + sb.toString());
		client.close();//关闭连接
		server.close();//关闭服务端
	}
}

结果如下:
初探socket编程 ——JavaSocket连接与简单通信_第4张图片

双向传递消息

想让消息有来有回,那么服务端就不能在接收后直接关闭连接,而是回复一条消息,那么这条消息就应该是通过服务端的输出流发送,在客户端的输入流接收。

/*客户端*/
public class SocketClient {
     
	public static void main(String[] args) throws UnknownHostException, IOException {
     
		System.out.println("客户端程序~~~");
		System.out.println("我创建了一个socket");
		Socket client = new Socket("127.0.0.1",8888);
		System.out.println("正在行服务端发送消息");
		/*向服务端发送消息*/
		client.getOutputStream().write("你好服务端,这里是客户端".getBytes());
		System.out.println("像服务端发送消息完毕");
		System.out.println("正在接收服务端回复");
		
		InputStream is = client.getInputStream();	//输入流
		byte[] buffer = new byte[1024];				//缓冲
		int len = 0;								//每次读取的长度(正常情况下是1024,最后一次可能不是1024,如果传输结束,返回-1)
		StringBuilder sb = new StringBuilder();		//构建读取的消息
		while((len = is.read(buffer)) != -1) {
     		//接收服务端的消息		
			sb.append(new String(buffer,0,len));
		}
		System.out.println("接收到服务端消息:" + sb.toString());
		/*会话结束*/
		client.close();
	}
}
/*服务端*/
public class SocketServer {
     
	public static void main(String[] args) throws IOException {
     
		System.out.println("服务端程序~~~");
		System.out.println("我创建了一个ServerSocket");
		ServerSocket server = new ServerSocket(8888);
		System.out.println("开始接受socket匹配");
		
		Socket client = server.accept();
		
		System.out.println("接收到了一个socket");
		
		InputStream is = client.getInputStream();	//输入流
		byte[] buffer = new byte[1024];				//缓冲
		int len = 0;								//每次读取的长度(正常情况下是1024,最后一次可能不是1024,如果传输结束,返回-1)
		StringBuilder sb = new StringBuilder();		//构建读取的消息
		while((len = is.read(buffer)) != -1){
     		//接收客户端的消息
			sb.append(new String(buffer,0,len));
		}
		System.out.println("收到客户端消息:" + sb.toString());
		System.out.println("正在回复~~~");
		client.getOutputStream().write("这里是服务端,收到消息,谢谢".getBytes());	//回复客户端
		
		client.close();//关闭连接
		server.close();//关闭服务端
	}
}

运行程序发现:
初探socket编程 ——JavaSocket连接与简单通信_第5张图片
客户端并没有接收到服务端的回复,而服务端停滞在了接收客户端信息的地方。

流的阻塞问题

实际上,问题就出现在客户端向服务端传输信息这一过程中。在服务端看来,虽然已经获得了所有客户端发来的字节,但是它并不能确定客户端是否要继续发送信息,因此输入流就卡在那里,形成了阻塞。

解决这个问题,一个粗暴地方式是在客户端直接关闭输出流,当然不是调用输出流的关闭方法,而是调用socket的shutdownOutput方法。这个方式有个缺点,就是在关闭输出流之后,将无法再次输出。

我们使用这个语句来完善刚刚客户端的程序

public class SocketClient {
     
	public static void main(String[] args) throws UnknownHostException, IOException {
     
		System.out.println("客户端程序~~~");
		System.out.println("我创建了一个socket");
		Socket client = new Socket("127.0.0.1",8888);
		System.out.println("正在行服务端发送消息");
		/*向服务端发送消息*/
		client.getOutputStream().write("你好服务端,这里是客户端".getBytes());
		System.out.println("像服务端发送消息完毕");
		System.out.println("正在接收服务端回复");
		
		client.shutdownOutput();//关闭输出!!!!!
		
		InputStream is = client.getInputStream();	//输入流
		byte[] buffer = new byte[1024];				//缓冲
		int len = 0;								//每次读取的长度(正常情况下是1024,最后一次可能不是1024,如果传输结束,返回-1)
		StringBuilder sb = new StringBuilder();		//构建读取的消息
		while((len = is.read(buffer)) != -1) {
     		//接收服务端的消息		
			sb.append(new String(buffer,0,len));
		}
		System.out.println("接收到服务端消息:" + sb.toString());
		/*会话结束*/
		client.close();
	}
}

这回两端的通信就可以顺利进行了
初探socket编程 ——JavaSocket连接与简单通信_第6张图片

上传和下载文件

传输文件与传输消息没有本质区别,在计算机眼中他们都是一样的二进制字节流。

不同点在于,一个数据源格式是字符串而另一个也是输入流。

我们要做的,就是将这些流进行对接,还是画个图来表示:
初探socket编程 ——JavaSocket连接与简单通信_第7张图片
由于上传和下载这两个过程很类似,所以这里就仅实现一个文件上传的操作,进行演示。

在刚刚的通信基础上稍作修改:

/*客户端*/
public class TCPFileClient {
     
	public static void main(String[] args) throws UnknownHostException, IOException {
     
		Scanner scan = new Scanner(System.in);
		System.out.println("请输入要传递的文件全路径:");
		String filename = scan.next();
		
		FileInputStream fin = new FileInputStream(filename);//文件输出流,指向待传输文件
		
		System.out.println("正在尝试连接服务器");
		Socket client = new Socket("10.151.140.39",8888);
		System.out.println("服务器连接成功");
		OutputStream os = client.getOutputStream();		
		
		byte[] bytes = new byte[1024];
		int len = 0;
		long cnt = 0;//统计发送的字节数kb
		
		System.out.println("文件开始传输");
		while((len = fin.read(bytes)) != -1) {
     
			cnt++;
			if(cnt % (1 << 10) == 0) {
     
				System.out.println("已传输" + cnt / 1024 + "m");
			}
			os.write(bytes,0,len);
		}
		System.out.println("文件传输完成");
		InputStream is = client.getInputStream();
		client.shutdownOutput();
		
		StringBuffer sb = new StringBuffer();
		System.out.println("正在接受回复");
		while((len = is.read(bytes)) != -1) {
     
			
			sb.append(new String(bytes,0,len));
		}
		System.out.println("接收到回复:'" + sb + "'");
		
		
		fin.close();
		client.close();
	}
}
/*服务端*/
public class TCPFileServer {
     
	public static void main(String[] args) throws IOException {
     
		ServerSocket server = new ServerSocket(8888);
		
		System.out.println("正在检测目标文件路径是否存在");
		File file = new File("E:\\server");
		if(!file.exists()) {
     
			System.out.println("已创建不存在的文件夹");
			file.mkdirs();
		}else {
     
			System.out.println("检测成功,目标文件夹存在");
		}
		FileOutputStream fos;
		Socket client;
		int name = 0;
		while(true) {
     
			System.out.println("服务器正在等待接收文件~~");
			fos = new FileOutputStream(file + "\\" + name + ".rar");//文件输出流,指向硬盘存储区域
			
			client = server.accept();
			
			InputStream is = client.getInputStream();
			
			byte[] bytes = new byte[1024];
			int len = 0;
			long cnt = 0;	//统计接收字节数kb
			System.out.println("正在进行文件传输");
			while((len = is.read(bytes)) != -1) {
     
				cnt++;
				if(cnt % (1 << 10) == 0) {
     
					System.out.println("已接收" + cnt / 1024 + "m");
				}
				fos.write(bytes,0,len);
			}
			System.out.println("文件传输成功,正在回话");
			client.getOutputStream().write("收到文件,谢谢".getBytes());
			fos.close();
			client.close();
			if(name == 50)
				break;

		}
		server.close();
	}
}

运行一下:
初探socket编程 ——JavaSocket连接与简单通信_第8张图片
文件传输成功!!
由于服务端使用了循环,所以我们可以使得服务端一直处在接受文件的状态。


参考资料

  • Java 网络编程 之 socket 的用法与实现
  • 【JAVA开发】Socket套接字网络编程

你可能感兴趣的:(JAVA,Socket编程,socket,网络,java)