
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
目前的服务器不能保证通信的连续性,它会在发送完消息后关闭连接
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
// socket服务端
public class Server {
private ServerSocket serverSocket;
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void start(int port){
try{
// (监听socket)
// 绑定指定端口,使服务器的Socket在指定端口号上运行
serverSocket = new ServerSocket(port);
// (已连接socket)
// 服务器遇到accept进入阻塞,等待客户端发出连接
// 连接成功后,服务器将获得绑定到同一本地端口6666的新socket,用于传输数据
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以保持连接,最终客户端的输入流连接到服务端的输出流,服务器的输入流连接到客户端的输出流
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{
// 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
// 服务器接收连接后创建客户端socket
clientSocket = new Socket(ip, port);
// 获取socket的输入输出流,以便与服务端通信
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();
}
}
}
测试
先手动启动服务端,然后运行以上测试案例即可完成一次连接和一次消息发送
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下使用命令行关闭占用端口的进程
// 参看端口号含6666的条目
netstat -ano|findstr "6666"
// 根据pid查询对应的应用程序
tasklist|findstr "1828"
// 杀死进程
taskkill /f /pid 1828
持续连接优化
在前一个案例中,服务器会阻塞直到客户端连接它。在单个消息后,连接就会关闭,客户端和服务端无法持续沟通,因此仅仅会出现在ping请求中
如果要实现一个聊天服务器,客户端和服务端之间就需要连续的来回通信
服务端在优化中我在服务端创建一个while循环来连续观察传来消息的服务器输入流
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{
// (监听socket)
// 绑定指定端口,使服务器的Socket在指定端口号上运行
serverSocket = new ServerSocket(port);
// (已连接socket)
// 服务器遇到accept进入阻塞,等待客户端发出连接
// 连接成功后,服务器将获得绑定到同一本地端口4444的新socket,用于传输数据
clientSocket = serverSocket.accept();
// 输出流,可发送消息到客户端
out = new PrintWriter(clientSocket.getOutputStream(), true);
// 输入流,可接收客户端消息
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
// while循环连续观察从客户端传来消息的服务器输入流
// 直到读取到exit断开连接
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
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{
// 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
// 服务器接收连接后创建客户端socket
clientSocket = new Socket(ip, port);
// 获取socket的输入输出流,以便与服务端通信
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服务器运行以下测试案例
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以保持与多个客户端的连接
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoMultiSever {
private ServerSocket serverSocket;
public void start(int port){
try {
// 仍然使用一个socket在主线程中监听端口
serverSocket = new ServerSocket(port);
while (true){
// 每次循环中,accept会阻塞调用,直到新的客户端调用
// 连接成功后,EchoMultiServer会将已连接的socket委托给 EchoClientHandler
new EchoClientHandler(serverSocket.accept()).start();
}
}
catch (IOException e){
e.printStackTrace();
}
}
public void stop(){
try{
serverSocket.close();
}
catch (IOException e){
e.printStackTrace();
}
}
// 创建一个单独的线程EchoClientHandler
// 保存已连接的socket与客户端交流
public static class EchoClientHandler extends Thread{
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public EchoClientHandler(Socket socket){
this.clientSocket = socket;
}
// 线程执行start直到运行run方法,与目标客户端进行交流
// 其内部发生的情况与EchoSever相同
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);
}
}
客户端
客户端不需要进行优化修改,与上面的相同
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{
// 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
// 服务器接收连接后创建客户端socket
clientSocket = new Socket(ip, port);
// 获取socket的输入输出流,以便与服务端通信
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后,运行以下案例
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图解
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)