文章目录
Socket编程概念
Java的网络编程主要涉及到的内容是Socket编程,那么什么是Socket呢?
- Socket通常称作“套接字”就是
两台主机之间逻辑连接的端点。
,通常通过“套接字”向网络发出请求或者响应网络请求。
TPC/IP协议
是传输层协议
,主要解决数据如何在网络中传输
,而HTTP
是应用层协议
,主要解决如何包装数据。
Socket,本质上就是一组接口,是对TCP/IP协议的封装和应用
(程序员层面上)。- 使用Socket进行网络编程时,本质上就是
两个进程之间的网络通信
。其中一个进程必须充当服务端,它会主动监听某个指定的端口,
另一个进程必须充当客户端,它必须主动连接服务端的IP地址和指定端口
,如果连接成功,服务端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。
因此,当Socket连接成功地在服务器端和客户端之间建立后:
1. 对服务端
来说,它的Socket是指定的IP地址和指定的端口号
;
2. 对客户端
来说,它的Socket是它所在计算机的IP地址
和一个由操作系统分配的随机端口号
。
Socket通信步骤
Socket编程主要涉及到客户端
和服务端
两个方面:
-
首先是在
服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。
端口号的范围是0到65536
,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。 -
客户端请求与服务器进行连接的时候,根据
服务器的域名
或者IP地址,加上端口号,打开一个套接字。当
服务器接受连接后,
服务器和客户端之间的通信就像输入输出流一样进行操作`。
TCP通信能实现两台计算机之间的数据交互,通信的两端要严格区分为客户端(Client)
与服务端(Server)
。
两端通信时步骤:
- 服务端程序需要事先启动,等待客户端的连接。
- 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
- 客户端和服务端会建立一个逻辑连接,连接中包含一个对象即IO对象,于是客户端和服务端就可以使用IO对象进行通信,通信的数据不仅仅是字符,所以IO对象是字节流对象.
-
客户端和服务端进行一次交互,需要4个IO流对象
-
服务器端和客户端进行交互时,必须明确两件事:
- 多个客户端同时和服务器进行交互,服务器必须明确和哪个客户端进行交互。在服务器端有一个方法,叫accept,可以获取到客户端的请求对象。
- 多个客户端和服务器进行交互,就需要使用多个IO流对象。这里要注意的是,服务器是没有IO流的,服务器可以获取到请求的客户端对象Socket,使用每个客户端Socket中提供的IO流和客户端进行交互。服务器使用客户端的字节输出流获取客户端发送的数据。服务器使用客户端的字节输出流给客户端回写数据。
简单记忆:服务器使用客户端的流和客户端进行交互。
举个例子,就像我请你吃饭,但是没带钱,我向你借钱然后请你吃饭一样。
socket编程相关类
Socket、TCP和部分IP的功能都是由操作系统
提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如,Java提供的几个Socket相关的类就封装了操作系统提供的接口。
在Java中,提供了两个类用于实现TCP通信程序:
客户端
:java.net.Socket
类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。服务端
:java.net.ServerSocket
类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接。
Socket类
- 该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。
Socket构造方法
方法 | 描述 |
---|---|
public Socket() | 构造一个Socket,因为没有指定目标主机和端口,所以不会通过网络进行连接。 |
public Socket(String host, int port) | 通过一个主机和端口构建一个Socket。构造Socket的时候会连接目标主机,如果连接不到目标主机则会抛出IOException或UnknownHostException异常。 |
public Socket(Proxy proxy) | 通过一个代理构建一个未连接的Socket |
public Socket(InetAddress address, int port) |
通过一个InetAddress 和端口构建一个Socket,构造的时候也会进行连接目标主机 |
public Socket(String host, int port, InetAddress localAddr, int localPort) | 通过一个要连接的远程主机和端口,并指定从本地哪个ip和端口连接 |
public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) | 通过一个要连接的远程主机和端口,并指定从本地哪个ip和端口连接 |
小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。
Socket常用方法
方法 | 描述 |
---|---|
close() |
关闭此socke,一旦一个socket被关闭,它不可再使用。关闭此socket也将关闭相关的InputStream和OutputStream |
connect(SocketAddress endpoint) | 将此socket 连接到服务器,结合socket无参构造方法使用 |
connect(SocketAddress endpoint, int timeout) | 将此socket 连接到服务器,并指定一个超时值,结合socket无参构造方法使用 |
getInputStream() |
返回此socket 的输入流,如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道,关闭生成的InputStream也将关闭相关的Socket。 |
getOutputStream() |
返回此socket 的输出流,如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。关闭生成的OutputStream也将关闭相关的Socket |
shutdownOutput() | 禁用此套接字的输出流=>任何先前写出的数据将被发送,随后终止输出流 |
getRemoteSocketAddress() | 返回此socket连接到的远程地址 ,返回一个SocketAddress 对象 |
InetAddress getInetAddress() | 返回此socket连接到的远程IP ,返回一个InetAddress 对象 |
getPort() | 返回此socket连接到的远程端口 |
getLocalSocketAddress() | 返回此socket绑定的的本地地址 ,返回一个SocketAddress 对象 |
getLocalAddress() | 返回此socket连接到的的本地IP ,返回一个InetAddress 对象 |
getLocalPort() | 返回此socket 绑定到的本地端口 |
connect方法
connet放提供了两个方法,一个是传入SocketAddress
进行连接目标地址,另一个是通过SocketAddress 和超时等待时间
来连接目标地址。
- connect(SocketAddress endpoint)
- connect(SocketAddress endpoint, int timeout)
代码中使用了Socket的空构造函数进行构造Socket对象。空构造不会进行连接目标主机(因为没有设置目标地址),需要使用connet方法进行连接目标主机。Socket socket = new Socket(); SocketAddress socketAddr = new InetSocketAddress("www.baidu.com", 80); socket.connect(socketAddr); //socket.connect(socketAddr, 2000);
代理服务器
使用public Socket(Proxy proxy)构造方法
来构造一个使用proxy的socket。以后使用该socket的相关的网络操作都会通过代理服务器进行连接。
//构造代理服务器地址
SocketAddress sa = new InetSocketAddress("192.168.10.130", 808);
//构造Socket代理
Proxy proxy = new Proxy(Proxy.Type.SOCKS, sa);
//使用代理创建socket
Socket socket = new Socket(proxy);
//构造目标地址
SocketAddress socketAddr = new InetSocketAddress("www.baidu.com", 80);
//socket使用代理连接目标地址
socket.connect(socketAddr);
半关闭连接
如果想关闭socket的输入或输出则可以使用一下两个方法。
//当调用shutdownInput()时,则不允许再次从socket中读取数据。
public void shutdownInput() //关闭输入流
//当调用shutdownOutPut()方法后会告诉流已经输入完成,不允许再次输入。
//对方读取流时,会接受到流结束标志(会返回-1)。
public void shutdownOutput()//关闭输出流
但关闭输入或输出
是不会关闭socket的,因为他们不会释放本地端口,还需要调用socket的close()
方法来关闭socket。
//下面两个方法来判断socket的输入或输出流是否关闭。
public boolean isInputShutdown()//是否关闭输入流
public boolean isOutputShutdown()//是否关闭输出流
如果socket未连接(通过空构造方法构造的socket),也会返回false(未关闭状态)
判断socket是否关闭
//是否连接过目标地址
//isConnected方法并不是连接状态才返回true,而是只要连接过目标地址就返回true,哪怕已经关闭的socket也会返回true的。
public boolean isConnected()
//是否关闭过socket
//isClosed方法是判断socket是否调用close()方法关闭过socket。
public boolean isClosed()
如果使用空构造方法构建socket而不连接目标主机,还没调用close方法,该方法会返回true
。所以要判断该socket是否连接目标地址需要这样判断
//打开过连接,但还没有关闭连接
if(socket.isConnected() && !socket.isClosed()){
System.out.println("连接状态");
}else{
System.out.println("未打开连接或已经关闭连接");
}
设置Socket属性的相关方法
方法 | 描述 |
---|---|
setReuseAddress(boolean on) | 启用/禁用 SO_REUSEADDR 套接字选项。 |
setSoTimeout(int timeout) | 通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。 1. 该选项表示accept()方法等待客户端连接的时间,当accept()方法 在超时时间内没有获得连接,就会抛出SocketTImeoutException。 |
setReceiveBufferSize(int size) | 为从此 ServerSocket 接受的套接字的 SO_RCVBUF 选项设置默认建议值。 1.设置缓冲大小,和Socket的该选项一样 |
setReuseAddress()
- 和Socket的setReuseAddress(boolean on)方法一样,当网络上仍然有旧数据向ServerSocket传输,是否允许新的ServerSocket绑定到与旧的ServerSocket 同样的端口上,在某些操作系统上允许重用端口,有些则不允许。
- 很多服务器程序都是用固定的端口,当程序关闭后,端口可能还被占用一段时间,如果此时立刻重启服务器,服务器就会无法绑定端口,抛出BindException异常
- 为了确保不发生这种异常,就可以调用ServerSocket的setReuseAddress(boolean on)方法
if(!serverSocket.getResuseAddress()) { serverSocket.setReuseAddress(boolean on); }
ServerSocket类
- 该类实现了服务器套接字,该对象等待通过网络的请求
ServerSocket构造方法
方法 | 描述 |
---|---|
public ServerSocket() | 构造一个ServerSocket,但不绑定任何端口,所以也不能接受任何的请求连接。以后可以通过·bind()方法来进行绑定。 |
public ServerSocket(int port) |
通过一个端口来构造一个ServerSocket对象。默认的tcp队列大小为50,默认监听本地所有的ip地址(如果有多个网卡) |
public ServerSocket(int port, int backlog) | 通过一个端口和TCP队列大小来构造一个ServerSocket对象。默认监听本地所有的ip地址(如果有多个网卡)。 |
public ServerSocket(int port, int backlog, InetAddress bindAddr) | 通过一个端口、TCP队列大小和一个InetAddress 来构造一个ServerSocket对象。 |
一个服务器可能有多个网卡,多个ip地址,通过此构造传入一个InetAddress ,可以只监听从此网卡过来的请求连接。
比如:一个电脑上有两个网卡,一个是外网地址,一个是内网地址。为了安全此ServerSocket只允许监听内网ip地址的请求,而不接收外网请求。
- TCP分为全连接队列和半连接队列。这里说的是全连接队列
- 半连接队列:第一次握手,服务器收到客户端的请求时,该请求连接放到半连接队列中。
- 全连接队列:已经通过三次握手后,把当前连接信息存放到全连接队列中。全连接队列中的连接等待
ServerSocket.accpt()
处理。
-
上面参数中
port为端口号
,InteAddress为IP地址
,backlog表示请求队列的长度,backlog的作用是: 当有一个Socket向服务器发出连接请求时,此连接请求进入请求队列,当请求队列满,而又有Socket对象发出连接请求时,此连接会被拒绝,客户端抛出ConnectException。 -
除了第一种不带参数的构造方法外,其他构造方法都使是服务器与特定端口绑定。
如果无法绑定到对应端口,会抛出BindException,一般端口被占用,或者操作系统不允许普通用户绑定1~1023之间的端口
,就会造成此异常。 -
如果把port设置为0
,则端口由操作系统随机分配,这种端口称为匿名端口
,通常不会使用匿名端口,因为客户端需要明确服务器的端口才可以向服务器发出连接请求。 -
和Socket类一样,对于有多个IP的主机,我们采用
第四种构造方法
来设置项用的IP;
而第(1)种构造方法存在的意义
是,有的服务器选项需要在绑定端口之前设置,这个时候就需要先创建ServerSocket对象, 再设置选项,最后再绑定端口,
如以下例子:
ServerSocket serverSocket = new SernerSocket();
serverSocket.serReuseAddress(true); //设置ServerSocket选项
serverSocket.bind(new InteSocketAddress(8000)); //绑定端口
如果把上面代码改成下面这样,则这个选项设置无效
ServerSocket serverSocket = new SernerSocket(8000);
serverSocket.serReuseAddress(true);
ServerSocket常用方法
方法 | 描述 |
---|---|
public Socket accept() |
该方法会从全连接队列中获取一个客户端Socket请求。该方法是阻塞方法。如果当前没有请求的连接,则会一直阻塞,直到有客户端连接请求为止。 |
bind(SocketAddress endpoint) bind(SocketAddress endpoint, int backlog) |
通过无参构造的ServerSocket对象,需要bing方法进行绑定操作才能处理客户端的请求。 1.通过SocketAddress 进行绑定,默认TCP队列大小为50。 2.通过SocketAddress 和 TCP请求队列大小 两个参数 进行绑定。 |
InetAddress getInetAddress() | 获取本地地址,如果本地有多个ip,随机返回一个 |
int getLocalPort() | 获取绑定的端口,如果构造ServerSocket中默认端口传入一个0,则是随机生成一个端口,这时就需要使用此方法来获取端口信息。 |
SocketAddress getLocalSocketAddress() | 返回本地地址和端口 |
close() | 调用该方法使服务器释放端口,并断开所有与客户端之间的连接。当一个服务器运行结束时,即使没有执行该方法,也会释放端口,因此服务器并不一定要在结束之前执行close()方法。 |
accpt()方法
- 该方法从连接请求队列中获取一个客户端请求,然后创建与客户端连接的Socket对象,并返回。
- 如果队列中没有连接请求,此方法会一直等待下去,直到收到连接请求才返回,之后服务器从Socket对象中获取输入流和输出流和客户端交换数据,
- 如果发送数据时客户端断开连接,服务器会抛出SocketException异常,
通信完成后,应在finally块应关闭Socket对象,释放资源。
使用ServerSocket 判断当前系统已经占用的端口
public static void main(String[] args) throws Exception {
for(int port=1; port<65535; port++){
try{
ServerSocket s = new ServerSocket(port);
}catch(IOException e){
System.out.println("当前系统中已经使用的端口:"+port);
}
}
}
Socket编程步骤
InetAddress用来描述主机地址;
Socket用来创建两台主机之间的连接;
ServerSocket用来侦听来自客户端的请求;
Socket通常称作“套接字”,通常通过“套接字”向网络发出请求或者应答网络请求
- 服务端:创建ServerSocket对象,绑定监听端口
- 服务端:通过accept()方法监听客户端请求
- 客户端:创建Socket对象,指明需要连接的服务器的地址和端口号
- 客户端:连接建立后,通过输出流向服务器发送请求信息
- 服务端:连接建立后,通过输入流读取客户端发送的请求信息
- 服务端:通过输出流向客户端发送响应信息
- 客户端:通过输入流获取服务器相应的信息
- 注意:客户端、服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
创建服务端
public class SocketServer {
private static String CHARSET = "utf-8";
public static void main(String[] args) {
BufferedReader br = null;
BufferedWriter bw = null;
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务端初始化。。。。。");
//设置ServerSocket超时表示5秒内没有客户端与服务端已经建立连接,则会抛出异常
//serverSocket.setSoTimeout(5000);
// 监听客户端请求
Socket socket = serverSocket.accept();
//放在这里表示客户端与服务端已经建立连接,5秒内客户端没有发起请求,则会抛出异常
//socket.setSoTimeout(5000);
//服务器端都使用Socket中的getInputStream方法和getOutputStream方法获得输入流和输出流,进一步进行数据读写操作
br = new BufferedReader(new InputStreamReader(socket.getInputStream(), CHARSET));
bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), CHARSET));
while (true) {
String request = br.readLine();//read()和readLine()都会读取对端发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。或者到达流的末尾,这个时候分别返回-1和null。
System.out.println("服务端接收请求=>" + request);
System.out.print("服务端响应=>");
Scanner scanner = new Scanner(System.in);
String response = scanner.nextLine();
bw.write(response);
bw.newLine();
bw.flush();
if(response.equals("exit")){
System.out.println("服务端关闭连接");
socket.close();
serverSocket.close();
break;
}
/*System.out.println("服务端:isInputShutdown="+socket.isInputShutdown());
System.out.println("服务端:isOutputShutdown="+socket.isOutputShutdown());
System.out.println("服务端:isBound="+socket.isBound());
System.out.println("服务端:isConnected="+socket.isConnected());
System.out.println("服务端:isClosed="+socket.isClosed());*/
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//这里不能随便关闭流,否则会把socket也关闭了(因为后面还要发送数据,所以不能关闭流,不管是关闭输入输入其中之一,都会导致输入和输出都不能使用)
// 我这里是因为使用了循环,不手动结束循环不会走finally
try {
if (bw != null) {
bw.close();
}
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
创建客户端
public class SocketClient {
private static String HOST = "127.0.0.1";
private static int PROT = 8080;
private static String CHARSET = "utf-8";
public static void main(String[] args) {
BufferedWriter bw = null;
BufferedReader br = null;
try {
Socket clientSocket = new Socket(HOST, PROT);
System.out.println("客户端初始化。。。。。");
//放在这里表示5秒内客户端与服务端已经建立连接,但5秒内没有读取到服务端响应,则会抛出异常
//clientSocket.setSoTimeout(5000);
// 获取连接服务端的输入输出流,用于向服务器提交数据或者获取响应
bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), CHARSET));
br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), CHARSET));
// 在一次连接中循环与服务端进行交互
while (true) {
//客户端发起请求
System.out.print("客户端发起请求=>");
Scanner scanner = new Scanner(System.in);
String request = scanner.nextLine();
bw.write(request);
//因为在服务端使用的是readLine,所以如果不调用newLine,那么会一直阻塞
bw.newLine();
bw.flush();
//客户端单向断开与服务端的输出流,关闭之后客户端不能再向服务端进行输出
//clientSocket.shutdownOutput();
String reqponse = br.readLine();// read()和readLine()都会读取对端发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。或者到达流的末尾,这个时候分别返回-1和null。
System.out.println("客户端接受响应=>"+reqponse);
if(request.equals("exit")){
System.out.println("客户端关闭连接");
clientSocket.close();
break;
}
/*System.out.println("客户端:isInputShutdown="+clientSocket.isInputShutdown());
System.out.println("客户端:isOutputShutdown="+clientSocket.isOutputShutdown());
System.out.println("客户端:isBound="+clientSocket.isBound());
System.out.println("客户端:isConnected="+clientSocket.isConnected());
System.out.println("客户端:isClosed="+clientSocket.isClosed());*/
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//这里不能随便关闭流,否则会把socket也关闭了(因为后面还要发送数据,所以不能关闭流,不管是关闭输入输入其中之一,都会导致输入和输出都不能使用)
// 我这里是因为使用了循环,不手动结束循环不会走finally
try {
if (bw != null) {
bw.close();
}
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
小结,使用readLine()一定要注意:
- 读入的数据要注意有/r或/n或/r/n
- 没有数据时会阻塞,在数据流异常或断开时才会返回null
- 使用socket之类的数据流时,要避免使用readLine(),以免为了等待一个换行/回车符而一直阻塞
Java中Socket上的Read
操作阻塞问题
从上面的实例代码中可以看出read()是一个阻塞方法
- read()和readLine()都会读取
对端
发送过来的数据,如果无数据可读,就会阻塞直到有数据可读。
或者到达流的末尾
,这个时候分别返回-1和null
。这个特性使得编程非常方便也很高效。
但是这样也有一个问题,就是如何让程序从这两个方法的阻塞调用中返回。
-
发送方发送完数据后调用
Socket的shutdownOutput()
方法关闭输出流
,这样对端的输入流
上的read操作
就会返回-1
。注意: 不能调用
socket.getInputStream().close()
,这样会导致socket被关闭
。
当然如果不需要继续在socket上进行读操作,也可以直接关闭socket。
但是这个方法不能用于通信双方需要多次交互的情况。out.write("sender say hello socket".getBytes()); out.flush(); client.shutdownOutput(); //调用shutdown 通知对端请求完毕
这个解决方案缺点非常明显,
socket任意一端都依赖于对方调用shutdownOutput()来完成read返回 -1,如果任意一方没有执行shutdown函数那么就会出现问题。
所以一般我们都会在socket请求时设置连接的超时时间socket.setSoTimeout(5000)
以防止长时间没有响应造成系统瘫痪。 -
为了防止read操作造成程序永久挂起,还可以
给socket设置超时。
(根据我的经验,只有在Socket级别设置才有效)
如果read()方法在设置时间内没有读取到数据,就会抛出一个java.net.SocketTimeoutException异常。
例如下面的方法设定超时3秒 :socket.setSoTimeout(3000);
while (true) { server = serverSocket.accept(); System.out.println("server socket is start……"); server.setSoTimeout(5000); // ..... }
-
发送数据时,约定数据的首部固定字节数为数据长度。
这样读取到这个长度的数据后,就不继续调用read方法这种方式
优点
是不依赖对方调用shutdown方法
,响应较快
,缺点
是数据传输的最大字节数固定,需双方事先约定好长度,伸缩性差
。 -
发送数据时,双方约定结尾字符信息,在读取到相应信息时,客户端主动发送断开连接的信息,或者发送信号给服务端,由服务端断开连接。
如: 约定前几位返回数据byte[]长度大小或最后输出 \n 或 \r 作为数据传输终止符。
客户端
out.write("sender say hello socket \n".getBytes()); out.flush();
服务器端
//服务端 byte[] buf = new byte[1]; int size = 0; StringBuffer sb = new StringBuffer(); while (( size = in.read(buf,0,buf.length)) != -1) { String str = new String(buf); if(str.equals("\n")) { break; } sb.append(str); System.out.print(str); }
这种方式是对第3种方案的改良版
,但不得不说 这是目前socket数据传输的最常用处理read()阻塞的解决方案。
服务器和客户端之间交互时使用BufferedReader的阻塞问题
Socket java.net.SocketException: Connection reset的解决方案
java socket多线程通讯,解决read阻塞问题
通过设置标记为解决基于tcp协议的socket通信阻塞问题
Java Socket学习—多线程阻塞
java基于tcp协议的Socket通信(多线程)
Java使用多线程实现Socket多客户端的通信-my2
Java Socket TCP编程(Server端多线程处理)
java多线程实现多客户端socket通信
TCP的三次握手与四次挥手理解及面试题(很全面)
理解TCP/IP三次握手与四次挥手的正确姿势
TCP-三次握手和四次挥手简单理解