网络编程较为复杂,本文只介绍Java网络编程的基础,详细的Java网络编程后续会出专门的Java网络编程类型的文章。
概念介绍
计算机网络
计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
IP地址
互联网上没一台计算机都有一个唯一表示自己的表示,就是IP地址,用来标志网络中的一个通信实体的地址。通信实体可以是计算机,路由器等。
特殊的IP地址
127.0.0.1本机地址
192.168.0.0–192.168.255.255私有地址,属于非注册地址,专门为组织机构内部使用。
端口
IP是用来标志一台计算机,但是一台计算机上可能提供很多种应用程序,使用端口来区分这些应用程序。
端口是虚拟的概念,并不是说在主机上真有若干个端口。通过端口,可以在一个主机上运行多个网络应用程序。
端口范围0–65535,16位整数
端口分类
- 公认端口0-1023 比如80端口分配给www,21端口分配给FTP
- 注册端口1024-49151 分配给用户进程或应用程序
- 动态/私有端口49152-65535
IP和端口的关系
必须同时制定IP地址和端口号才能正确的发送数据
URL
在www上,每一信息资源都有统一且唯一的地址,该地址就叫URL(Uniform Resource Locator),它是www的统一资源定位符。
URL组成:协议 、存放资源的主机域名、资源文件名和端口号。
如果未指定该端口号,则使用协议默认的端口。例如http 协议的默认端口为 80。 在浏览器中访问网页时,地址栏显示的地址就是URL。
网络服务器
网络服务器是计算机局域网的核心部件。网络操作系统是在网络服务器上运行的,网络服务器的效率直接影响整个网络的效率。因此,一般要用高档计算机或专用服务器计算机作为网络服务器。
网络通信协议
主要分为TCP协议和UDP协议
TCP协议
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP,下一篇博客会实现)是同一层内 另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元( MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
特点:面向连接、点到点的通信、高可靠性:三次握手、占用系统资源多、效率低 应用案例:HTTP FTP TELNET SMTP
UDP协议
一种无连接的传输层协议,提供面向事物的简单不可靠信息传送服务 特点:非面向连接,传输不可靠,可能丢失、发送不管对方是否准备好,接收方收到也不确认、可以广播发送、非常简单的协议,开销小 应用案例:DNS SNMP 传输声音,视频信号,很多网络游戏中
Socket
开发的网络应用程序位于应用层,TCP和UDP属于传输层协议,在应用层如何使用传输层的服务呢?在应用层和传输层之间,则是使用套接Socket来进行分离。
套接字就像是传输层为应用层开的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,是不知道也不需要知道的,也不会关心它如何传输,这属于网络其它层次工作。
Socket实际是传输层供给应用层的编程接口。Socket就是应用层与传输层之间的桥梁。使用Socket编程可以开发客户机和服务器应用程序,可以在本地网络上进行通信,也可通过Internet在全球范围内通信。
Socket整体流程
Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。
客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
Java网络编程
基于Socket的TCP编程
构造ServerSocket
构造方法:
-
ServerSocket() ~创建非绑定服务器套接字。 -
ServerSocket(int port) ~创建绑定到特定端口的服务器套接字。 -
ServerSocket(int port, int backlog) ~利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。 -
ServerSocket(int port, int backlog, InetAddress bindAddr) ~使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。
1.1 绑定端口 除了第一个不带参数的构造方法以外, 其他构造方法都会使服务器与特定端口绑定, 该端口有参数 port 指定. 例如, 以下代码创建了一个与 80 端口绑定的服务器:
ServerSocket serverSocket = new ServerSocket(80);
如果运行时无法绑定到 80 端口, 以上代码会抛出 IOException, 更确切地说, 是抛出 BindException, 它是 IOException 的子类. BindException 一般是由以下原因造成的:
- 端口已经被其他服务器进程占用;
- 在某些操作系统中, 如果没有以超级用户的身份来运行服务器程序, 那么操作系统不允许服务器绑定到 1-1023 之间的端口.
如果把参数 port 设为 0, 表示由操作系统来为服务器分配一个任意可用的端口. 有操作系统分配的端口也称为匿名端口. 对于多数服务器, 会使用明确的端口, 而不会使用匿名端口, 因为客户程序需要事先知道服务器的端口, 才能方便地访问服务器.
1.2 设定客户连接请求队列的长度 当服务器进程运行时, 可能会同时监听到多个客户的连接请求. 例如, 每当一个客户进程执行以下代码:
Socket socket = new Socket("www.javathinker.org", 80);
就意味着在远程 www.javathinker.org 主机的 80 端口上, 监听到了一个客户的连接请求. 管理客户连接请求的任务是由操作系统来完成的. 操作系统把这些连接请求存储在一个先进先出的队列中. 许多操作系统限定了队列的最大长度, 一般为 50 . 当队列中的连接请求达到了队列的最大容量时, 服务器进程所在的主机会拒绝新的连接请求. 只有当服务器进程通过 ServerSocket 的 accept() 方法从队列中取出连接请求, 使队列腾出空位时, 队列才能继续加入新的连接请求.
对于客户进程, 如果它发出的连接请求被加入到服务器的请求连接队列中, 就意味着客户与服务器的连接建立成功, 客户进程从 Socket 构造方法中正常返回. 如果客户进程发出的连接请求被服务器拒绝, Socket 构造方法就会抛出 ConnectionException.
Tips: 创建绑定端口的服务器进程后, 当客户进程的 Socket构造方法返回成功, 表示客户进程的连接请求被加入到服务器进程的请求连接队列中. 虽然客户端成功返回 Socket对象, 但是还没跟服务器进程形成一条通信线路. 必须在服务器进程通过 ServerSocket 的 accept() 方法从请求连接队列中取出连接请求, 并返回一个Socket 对象后, 服务器进程这个Socket 对象才与客户端的 Socket 对象形成一条通信线路.
ServerSocket 构造方法的 backlog 参数用来显式设置连接请求队列的长度, 它将覆盖操作系统限定的队列的最大长度. 值得注意的是, 在以下几种情况中, 仍然会采用操作系统限定的队列的最大长度:
- backlog 参数的值大于操作系统限定的队列的最大长度;
- backlog 参数的值小于或等于0;
- 在ServerSocket 构造方法中没有设置 backlog 参数.
以下的 Client.java 和 Server.java 用来演示服务器的连接请求队列的特性.
Client.java
import java.net.Socket;
public class Client {
public static void main(String[] args) throws Exception{
final int length = 100;
String host = "localhost";
int port = 1122;
Socket[] socket = new Socket[length];
for(int i = 0;i<length;i++){
socket[i] = new Socket(host,port);
System.out.println("第"+(i+1)+"次连接成功!");
}
Thread.sleep(3000);
for(int i=0;i<length;i++){
socket[i].close();
}
}
}
Server.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private int port = 1122;
private ServerSocket serverSocket;
public Server() throws Exception{
serverSocket = new ServerSocket(port,3);
System.out.println("服务器启动!");
}
public void service(){
while(true){
Socket socket = null;
try {
socket = serverSocket.accept();
System.out.println("New connection accepted "+
socket.getInetAddress()+":"+socket.getPort());
} catch (IOException e) {
e.printStackTrace();
}finally{
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws Exception{
Server server = new Server();
Thread.sleep(60000*10);
server.service();
}
}
⑴ 在Server 中只创建一个 ServerSocket 对象, 在构造方法中指定监听的端口为1122 和 连接请求队列的长度为 3 . 构造 Server 对象后, Server 程序睡眠 10 分钟, 并且在 Server 中不执行 serverSocket.accept() 方法. 这意味着队列中的连接请求永远不会被取出. 运行Server 程序和 Client 程序后, Client程序的打印结果如下: 第 1 次连接成功 第 2 次连接成功 第 3 次连接成功 Exception in thread “main” java.net.ConnectException: Connection refused: connect ……………. 从以上打印的结果可以看出, Client 与 Server 在成功地建立了3 个连接后, 就无法再创建其余的连接了, 因为服务器的队已经满了.
⑵ 在Server中构造一个跟 ⑴ 相同的 ServerSocket对象, Server程序不睡眠, 在一个 while 循环中不断执行 serverSocket.accept()方法, 该方法从队列中取出连接请求, 使得队列能及时腾出空位, 以容纳新的连接请求. Client 程序的打印结果如下: 第 1 次连接成功 第 2 次连接成功 第 3 次连接成功 ………… 第 100 次连接成功 从以上打印结果可以看出, 此时 Client 能顺利与 Server 建立 100 次连接.(每次while的循环要够快才行, 如果太慢, 从队列取连接请求的速度比放连接请求的速度慢的话, 不一定都能成功连接)
1.3 设定绑定的IP 地址 如果主机只有一个IP 地址, 那么默认情况下, 服务器程序就与该IP 地址绑定. ServerSocket 的第 4 个构造方法 ServerSocket(int port, int backlog, InetAddress bingAddr) 有一个 bindAddr 参数, 它显式指定服务器要绑定的IP 地址, 该构造方法适用于具有多个IP 地址的主机. 假定一个主机有两个网卡, 一个网卡用于连接到 Internet, IP为 222.67.5.94, 还有一个网卡用于连接到本地局域网, IP 地址为 192.168.3.4. 如果服务器仅仅被本地局域网中的客户访问, 那么可以按如下方式创建 ServerSocket:
ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName(“192.168.3.4”));
1.4 默认构造方法的作用 ServerSocket 有一个不带参数的默认构造方法. 通过该方法创建的 ServerSocket 不与任何端口绑定, 接下来还需要通过 bind() 方法与特定端口绑定.
这个默认构造方法的用途是, 允许服务器在绑定到特定端口之前, 先设置ServerSocket 的一些选项. 因为一旦服务器与特定端口绑定, 有些选项就不能再改变了.比如:SO_REUSEADDR 选项
在以下代码中, 先把 ServerSocket 的 SO_REUSEADDR 选项设为 true, 然后再把它与 8000 端口绑定:
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(8000));
如果把以上程序代码改为:
ServerSocket serverSocket = new ServerSocket(8000);
serverSocket.setReuseAddress(true);
那么 serverSocket.setReuseAddress(true) 方法就不起任何作用了, 因为 SO_REUSEADDR 选项必须在服务器绑定端口之前设置才有效.
UDP网络编程
UDP代码主要是4步
- 创建DatagramSocket对象,也就是创建socket服务
- 创建发送/接收DatagramPacket数据包,其中DatagramPacket方法中有4个参数:1.字节数据 2.数据长度 3.目的IP 4.端口号
- 发送/接收数据
- 关闭Socket
发送数据
import java.io.IOException;
import java.net.*;
public class Send {
public static void main(String args[])throws IOException{
DatagramSocket socket = new DatagramSocket();
try{
String str = "UDP send data is 1";
DatagramPacket packet = new DatagramPacket(str.getBytes(), str.getBytes().length, InetAddress.getByName("localhost"), 2020);
socket.send(packet);
}catch (Exception e){
}finally {
socket.close();
}
}
}
接收数据
import java.io.IOException;
import java.net.*;
public class Rec {
public static void main(String args[])throws IOException{
DatagramSocket socket = new DatagramSocket(2020);
byte[] buf = new byte[1024];
try{
DatagramPacket Recpacket = new DatagramPacket(buf, buf.length);
socket.receive(Recpacket);
System.out.println(new String(buf, 0, Recpacket.getLength()));
}catch (Exception e){
}finally {
socket.close();
}
}
}
|