概述 Socket编程是指编写在多台计算机上执行的程序,其中的设备都使用网络相互连接
Socket常用的通信协议有UDP和TCP,本文主要介绍通过TCP/IP网络协议进行Socket编程
Socket通信流程
服务端和客户端初始化 socket
,得到文件描述符;
服务端调用 bind
,将绑定在 IP 地址和端口;
服务端调用 listen
,进行监听;
服务端调用 accept
,等待客户端连接;
客户端调用 connect
,向服务器端的地址和端口发起连接请求;
服务端 accept
返回用于传输的 socket
的文件描述符;
客户端调用 write
写入数据;服务端调用 read
读取数据;
客户端断开连接时,会调用 close
,那么服务端 read
读取数据的时候,就会读取到了 EOF
,待处理完数据后,服务端调用 close
,表示连接关闭;
服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket ,一个叫作已完成连接 socket 。
简单示例 Socket是网络上不同计算机运行的两个程序之间双向通信链路的一个端点。Socket需要绑定端口号,一遍传输层可以标识数据要发送到的应用程序
服务端 服务端会用到两个socket,一个叫作监听 socket ,一个叫作已完成连接 socket
目前的服务器不能保证通信的连续性,它会在发送完消息后关闭连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import java.io.*;import java.net.ServerSocket;import java.net.Socket;public class Server { private ServerSocket serverSocket; private Socket clientSocket; private PrintWriter out; private BufferedReader in; public void start (int port) { try { serverSocket = new ServerSocket(port); clientSocket = serverSocket.accept(); out = new PrintWriter(clientSocket.getOutputStream(), true ); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String greeting = in.readLine(); if (greeting.equals("hello server" )){ out.println("hello client" ); } else { out.println("unrecognised greeting" ); } } catch (IOException e){ e.printStackTrace(); } } public void stop () { try { in.close(); out.close(); clientSocket.close(); serverSocket.close(); } catch (IOException e){ e.printStackTrace(); } } public static void main (String[] args) { Server server = new Server(); server.start(6666 ); } }
客户端 客户端只需要创建一个socket以保持连接,最终客户端的输入流连接到服务端的输出流,服务器的输入流连接到客户端的输出流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import com.sun.javafx.iio.ios.IosDescriptor;import java.io.*;import java.net.Socket;public class Client { private Socket clientSocket; private PrintWriter out; private BufferedReader in; public void startConnection (String ip, int port) { try { clientSocket = new Socket(ip, port); out = new PrintWriter(clientSocket.getOutputStream(), true ); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); } catch (IOException e){ e.printStackTrace(); } } public String sendMessage (String msg) { out.println(msg); String resp = null ; try { resp = in.readLine(); } catch (IOException e) { e.printStackTrace(); } return resp; } public void stopConnection () { try { in.close(); out.close(); clientSocket.close(); } catch (IOException e){ e.printStackTrace(); } } }
测试 先手动启动服务端,然后运行以上测试案例即可完成一次连接和一次消息发送
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.junit.Test;public class HelloTest { @Test public void hello () { Client client = new Client(); client.startConnection("127.0.0.1" , 6666 ); String response = client.sendMessage("hello server" ); System.out.println(response); } }
若启动服务端时出现以下报错,是出现了端口占用,可以修改端口也可以关闭占用端口的进程
Windows下使用命令行关闭占用端口的进程
1 2 3 4 5 6 7 8 // 参看端口号含6666的条目 netstat -ano|findstr "6666" // 根据pid查询对应的应用程序 tasklist|findstr "1828" // 杀死进程 taskkill /f /pid 1828
持续连接优化 在前一个案例中,服务器会阻塞直到客户端连接它。在单个消息后,连接就会关闭,客户端和服务端无法持续沟通,因此仅仅会出现在ping请求中
如果要实现一个聊天服务器,客户端和服务端之间就需要连续的来回通信
服务端 在优化中我在服务端创建一个while循环来连续观察传来消息的服务器输入流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;public class EchoServer { private ServerSocket serverSocket; private Socket clientSocket; private PrintWriter out; private BufferedReader in; public void start (int port) { try { serverSocket = new ServerSocket(port); clientSocket = serverSocket.accept(); out = new PrintWriter(clientSocket.getOutputStream(), true ); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null ){ if (inputLine.equals("exit" )){ out.println("goodbye" ); break ; } out.println(inputLine.replace("req" , "res" )); } } catch (IOException e){ e.printStackTrace(); } } public void stop () { try { in.close(); out.close(); clientSocket.close(); serverSocket.close(); } catch (IOException e){ e.printStackTrace(); } } public static void main (String[] args) { EchoServer server = new EchoServer(); server.start(4444 ); } }
客户端 客户端不需要进行优化修改,这里为了方区分创建一个新的类EchoClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import java.io.*;import java.net.Socket;public class EchoClient { private Socket clientSocket; private PrintWriter out; private BufferedReader in; public void startConnection (String ip, int port) { try { clientSocket = new Socket(ip, port); out = new PrintWriter(clientSocket.getOutputStream(), true ); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); } catch (IOException e){ e.printStackTrace(); } } public String sendMessage (String msg) { out.println(msg); String resp = null ; try { resp = in.readLine(); } catch (IOException e) { e.printStackTrace(); } return resp; } public void stopConnection () { try { in.close(); out.close(); clientSocket.close(); } catch (IOException e){ e.printStackTrace(); } } }
测试 在初始示例中,我们只在服务器关闭连接之前进行一次通信。现在,我们发送一个终止信号,以便在会话完成时告诉服务器,以此关闭服务器的socket进程
开启EchoServer服务器运行以下测试案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import org.junit.After;import org.junit.Before;import org.junit.Test;public class EchoTest { private EchoClient client = new EchoClient(); @Before public void setup () { client.startConnection("127.0.0.1" , 4444 ); } @After public void tearDown () { client.stopConnection(); } @Test public void echo () { String resp1 = client.sendMessage("req:hello" ); String resp2 = client.sendMessage("req:world" ); String resp3 = client.sendMessage("exit" ); System.out.println(resp1); System.out.println(resp2); System.out.println(resp3); } }
@BeforeClass – 表示在类中的任意public static void方法执行之前执行
@AfterClass – 表示在类中的任意public static void方法执行之后执行
@Before – 表示在任意使用@Test注解标注的public void方法执行之前执行
@After – 表示在任意使用@Test注解标注的public void方法执行之后执行
@Test – 使用该注解标注的public void方法会表示为一个测试方法
多客户端优化 在实际情况中,服务端常常要处理多个客户端的请求,为此我们要在服务端为每一个客户端请求创建一个新的socket线程,即提供服务的客户端数将等于服务端正在运行的线程数
服务端 仍然用一个监听socket 在主线程监听端口,而需要多线程存储已连接socket 以保持与多个客户端的连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import java.io.*;import java.net.ServerSocket;import java.net.Socket;public class EchoMultiSever { private ServerSocket serverSocket; public void start (int port) { try { serverSocket = new ServerSocket(port); while (true ){ new EchoClientHandler(serverSocket.accept()).start(); } } catch (IOException e){ e.printStackTrace(); } } public void stop () { try { serverSocket.close(); } catch (IOException e){ e.printStackTrace(); } } public static class EchoClientHandler extends Thread { private Socket clientSocket; private PrintWriter out; private BufferedReader in; public EchoClientHandler (Socket socket) { this .clientSocket = socket; } public void run () { try { out = new PrintWriter(clientSocket.getOutputStream(), true ); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null ){ if (inputLine.equals("exit" )){ out.println("bye" ); break ; } out.println(inputLine.replace("req" , "res" )); } in.close(); out.close(); clientSocket.close(); } catch (IOException e){ e.printStackTrace(); } } } public static void main (String[] args) { EchoMultiSever server = new EchoMultiSever(); server.start(5555 ); } }
客户端 客户端不需要进行优化修改,与上面的相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import java.io.*;import java.net.Socket;public class EchoMultiClient { private Socket clientSocket; private PrintWriter out; private BufferedReader in; public void startConnection (String ip, int port) { try { clientSocket = new Socket(ip, port); out = new PrintWriter(clientSocket.getOutputStream(), true ); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); } catch (IOException e){ e.printStackTrace(); } } public String sendMessage (String msg) { out.println(msg); String resp = null ; try { resp = in.readLine(); } catch (IOException e) { e.printStackTrace(); } return resp; } public void stopConnection () { try { in.close(); out.close(); clientSocket.close(); } catch (IOException e){ e.printStackTrace(); } } }
测试 测试类中需要发起多个客户端请求
运行EchoMultiSever后,运行以下案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import org.junit.Test;public class EchoMultiTest { @Test public void buildClient1 () { EchoClient client1 = new EchoClient(); client1.startConnection("127.0.0.1" , 5555 ); String resp1 = client1.sendMessage("req:hello" ); String resp2 = client1.sendMessage("req:world" ); String resp3 = client1.sendMessage("exit" ); System.out.println(resp1); System.out.println(resp2); System.out.println(resp3); } @Test public void buildClient2 () { EchoClient client2 = new EchoClient(); client2.startConnection("127.0.0.1" , 5555 ); String resp1 = client2.sendMessage("req:hello" ); String resp2 = client2.sendMessage("req:world" ); String resp3 = client2.sendMessage("exit" ); System.out.println(resp1); System.out.println(resp2); System.out.println(resp3); } @Test public void buildClient3 () { EchoClient client3 = new EchoClient(); client3.startConnection("127.0.0.1" , 5555 ); String resp1 = client3.sendMessage("req:hello" ); String resp2 = client3.sendMessage("req:world" ); String resp3 = client3.sendMessage("exit" ); System.out.println(resp1); System.out.println(resp2); System.out.println(resp3); } }
参考文章 Java 套接字
TCP/IP图解