Java语言作为最流行的网络编程语言,提供了强大的网络编程功能。
使用Java语言可以编写底层的网络通信程序,这是通过java.net包中提供的InetAddress类、URL类、Socket类以及ServerSocket等类实现的。
1、网络地址类InetAddress
要实现网络通信,首先需要知道计算机的地址。连接到Internet上的计算机使用IP地址或域名来唯一标识,在局域网上的计算机则可以使用名称标识。在java.net包中提供了InetAddress类来表示计算机地址。
InetAddress类没有提供构造方法,要得到一个InetAddress类对象,需要使用该类的静态方法。
eg:
public class InetAddressDemo {
public static void main(String[] args) {
try {
InetAddress addr = InetAddress.getLocalHost();
System.out.println(addr.getHostAddress());
System.out.println(addr.getHostName());
System.out.println(addr.getAddress().length);
InetAddress addr2 = InetAddress.getByName("www.acfun.cn");
System.out.println(addr2.getHostAddress());
System.out.println(addr2.getHostName());
System.out.println(addr2.getAddress().length);
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
2、 URL编程
2.1 URL与URL类
URL是统一资源定位器(Uniform Resource Locator)的简写,它是WWW中网络资源定位的表示方法。
URL的基本格式为:
<协议名://><主机名>[<:端口号>]</资源名>
- 协议名表示资源使用的协议,如http、ftp、telnet、mailto或file等。
- 主机名为任何合法的主机域名,如www.njupt.edu.cn。
- 端口号是可选的,如果使用熟知端口号,则可以省略。
- 资源名一般用来指定远程主机上文件系统中文件的完整路径,如/index.html。
URL类的实现:
static void Url1()
{
try {
String s = "https://www.baidu.com/";
URL url = new URL(s);
System.out.println("Protocol:" + url.getProtocol()); //获取协议名
System.out.println("Host:" + url.getHost()); //获取主机号
System.out.println("Port:" + url.getPort());//获取端口号,若没有指定端口号则返回-1
System.out.println("DefaultPort:" + url.getDefaultPort()); //获取默认端口号
System.out.println("File:" + url.getFile()); //返回URL的文件名及路径
InputStream is = url.openStream();
InputStreamReader isr = new InputStreamReader(is,"utf-8");
int c = 0;
while( ( c = isr.read() ) != -1)
{
System.out.print((char)c);
}
isr.close();
is.close();
System.out.println("finish");
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
2.2 URL的Connection类
- 通过URL的openStream()方法,只能获得InputStream对象。使用该对象只能从网络上读取数据。
- 如果希望不仅要从URL读取内容,还要向URL对象发送服务请求及参数,可以使用URLConnection类。
- 利用URL类提供的openConnection()方法,可以建立一个URLConnection对象。可以调用其getInputStream()方法和getOutputStream()方法得到输入流和输出流对象。
eg:
static void Url2()
{
String surl = "https://www.baidu.com";
try {
URL url = new URL(surl);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true); //设置允许上传
//http请求的一些属性
conn.setRequestProperty("ContentType", "text/plain");
//打开连接输出流,输出数据
OutputStream os = conn.getOutputStream();
FileInputStream fis = new FileInputStream("test.txt");
int c = 0;
System.out.println((char)fis.read());
while( (c=fis.read()) != -1)
{
os.write(c);
}
fis.close();
os.close();
//当打开输入流的时候,请求发出,并得到响应
InputStream is = conn.getInputStream();
InputStreamReader reader = new InputStreamReader(is,"utf-8");
c = 0;
while( (c=reader.read()) != -1) {
System.out.print((char)c);
}
reader.close();
is.close();
System.out.println("read finish");
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
3、Java套接字编程
3.1 端口号和套接字
在网络上,很多应用都是采用客户/服务器(C/S)结构的。实现网络通信必须将两台机器连接起来建立一个双向通信链路,这个双向通信链路的每一端称为一个套接字(Socket)。
在Internet上使用IP地址唯一标识一台主机。
一台主机可能提供多种服务,仅用IP地址不能唯一标识一个服务。通常使用一个整数来标识该机器上的某个服务,这个整数就是端口号(Port)。
端口号是用16位整数标识,共有65536个端口号。端口号并不是机器上实际存在的物理位置,而是一种软件上的抽象。
一个TCP连接由它的两个端点来标识,而每一个端点又是由IP地址和端口号决定的。TCP连接的端点称为套接字,套接字是由IP地址和端口号构成的,如下图所示。
这里,131.6.23.13为IP地址,1500为端口号,因此套接字为131.6.23.13,1500。
3.2 套接字通信
运行在一台特定机器上的某个服务器(如HTTP服务器)都有一个套接字绑定到该服务器上。服务器只是等待、监听客户的连接请求。
在客户端,客户机需要知道服务器的主机名和端口号。为了建立连接请求,客户机试图与服务器机上的指定端口号上的服务连接,这个请求过程如下图所示。
如果正常,服务器将接受连接请求。一旦接受了请求,服务器将创建一个新的绑定到另一个端口号的套接字,然后使用该套接字与客户通信。这样,服务器可以使用原来的套接字继续监听连接请求,如图所示。
在客户端,如果连接被接受,就会创建一个套接字,客户就可以使用该套接字与服务器通信。注意,客户端的套接字并没有绑定到与服务器连接的端口号上,相反客户被指定客户程序所在机器上的一个端口号上。现在客户与服务器就可以读写套接字进行通信了。
3.3 套接字类
为了实现套接字通信,在java.net包中提供了两个类:Socket和ServerSocket。它们分别实现连接的客户端和服务器端的套接字。
3.3.1 Socket类
Client和Server都需定义出该类的对象。创建对象时,端口号必须对应。
public InputStrean getInputStream() throws IOException
获得套接字绑定的数据输入流。 public OutputStream getOutputStream() throws IOException
获得套接字绑定的数据输出流。
3.3.2 ServerSocket类
用在服务器端。客户与服务器通信,客户向服务器提出请求,服务器监听请求,一旦监听到客户请求,服务器也要建立一个套接字。
3.3.3 客户和服务器通信的实例
包括以下4个基本步骤:
(1)创建套接字对象。
(2)打开连接到套接字的输入输出流。
(3)按照一定协议对套接字进行读写操作。
(4)关闭套接字对象。
eg:
服务器代码:
public class Server {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
//服务器监听在本机的8000端口
ServerSocket server = new ServerSocket(8000);
System.out.println("server start..");
//当接受到客户端请求后,生成一个socket,用于和客户端的数据传输
Socket socket = server.accept();//阻塞,等待客户端连接请求
System.out.println(socket.getRemoteSocketAddress());//得到Client的Socket地址
System.out.println(socket.getLocalSocketAddress());//得到Server的Socket地址
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
os.flush();
byte[] bts = new byte[20];
//采用的是阻塞IO(若没有数据,就一直等待)
int count = 0;
String str = "";
Scanner scanner = new Scanner(System.in);
while( true )
{
count = is.read(bts, 0, 20);
str = new String(bts,0,count);
System.out.println("Client say:"+str);
if( str.equals("bye"))
break;
str = scanner.nextLine();
os.write(str.getBytes());
os.flush();
if( str.equals("bye") )
break;
}
is.close();
os.close();
System.out.println("server stop..");
server.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端代码:
public class Client {
public static void main(String[] args) {
try {
//参数是要连接的服务器地址和端口,发出连接请求,建立连接
Socket socket = new Socket("localhost",8000);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
byte[] bts = new byte[20];
int count = is.read(bts, 0, 20);
System.out.println("Server say:"+new String(bts,0,20));
count = 0;
String str = "";
Scanner scanner = new Scanner(System.in);
while( true )
{
str = scanner.nextLine();
os.write(str.getBytes());
os.flush();
if( str.equals("bye") )
break;
count = is.read(bts, 0, 20);
str = new String(bts,0,count);
System.out.println("Server say:"+str);
if( str.equals("bye"))
break;
}
is.close();
os.close();
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
3.3.4 多个客户端和服务器通信实例
Server:
public class ServerDemo {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(9000);
while( true )
{
Socket socket = server.accept();
//启动客户端线程,独立完成通信
new Thread(new ClientThreadDemo(socket)).start();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("server start fail..");
}finally {
try {
if(server !=null)
server.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Client:
public class Client {
public static void main(String[] args) {
try {
//参数是要连接的服务器地址和端口,发出连接请求,建立连接
Socket socket = new Socket("localhost",9000);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
byte[] bts = new byte[20];
int count = is.read(bts, 0, 20);
System.out.println("Server say:"+new String(bts,0,20));
count = 0;
String str = "";
Scanner scanner = new Scanner(System.in);
while( true )
{
str = scanner.nextLine();
os.write(str.getBytes());
os.flush();
if( str.equals("bye") )
break;
count = is.read(bts, 0, 20);
str = new String(bts,0,count);
System.out.println("Server say:"+str);
if( str.equals("bye"))
break;
}
is.close();
os.close();
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
ClientThread:
public class ClientThreadDemo implements Runnable
{
private Socket socket;
public ClientThreadDemo(Socket socket)
{
this.socket = socket;
}
@Override
public void run() {
// TODO Auto-generated method stub
InputStream is = null;
OutputStream os = null;
try
{
is = socket.getInputStream();
os = socket.getOutputStream();
os.write("hello".getBytes());
os.flush();
byte[] bts = new byte[20];
//采用的是阻塞IO(若没有数据,就一直等待)
int count = 0;
String str = "";
Scanner scanner = new Scanner(System.in);
while( true )
{
count = is.read(bts, 0, 20);
str = new String(bts,0,count);
System.out.println("Client say:"+str);
if( str.equals("bye"))
break;
str = scanner.nextLine();
os.write(str.getBytes());
os.flush();
if( str.equals("bye") )
break;
}
}catch (Exception e) {
e.printStackTrace();
}finally {
try {
if(is != null) is.close();
if(os != null) os.close();
if(socket != null) socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
4、数据报通信
4.1 流式通信和数据报通信
当编写网络程序时,有两种通信可供选择:流式通信和数据报通信。
流式通信 | 数据报通信 |
---|---|
流式通信使用TCP协议,该协议是面向连接的协议。使用这种协议要求发送方和接收方都要建立套接字,一旦两个套接字建立起来,它们就可以进行双向通信,双方都可以发送和接收数据。 | 数据报通信使用UDP协议,该协议是一种无连接的协议。使用这种协议通信,每个数据报都是一个独立的信息单元,它包括完整的目的地址,数据报在网络上以任何可能的路径传往目的地,因此数据能否到达目的地、到达的时间以及内容的正确性都是不能保证,该协议提供的是不可靠的服务。 |
对于TCP,是一个面向连接的协议,在通信之前必须建立双方的连接,因此在TCP中多了一个建立连接的时间。 | 使用UDP时,每个数据报都给出了完整的地址信息,无需建立发送方和接收方的连接。 |
使用TCP就没有大小限制,一旦连接建立,就可以传输大量数据。 | 使用UDP传输数据时有大小限制,每个数据报必须不大于64KB。 |
TCP是可靠协议,确保接收方完全正确地获取发送方发送的数据。 | UDP是不可靠协议,发送方发送的数据不一定以相同的次序到达接收方。 |
TCP使用较广泛,如TELNET远程登录、FTP文件传输都需要不定长度的数据可靠地传输,因此使用TCP协议。 | UDP比较简单,需要较少的监护,因此常用于局域网分散系统中的客户/服务器应用程序。 |
4.2 DatagramSocket和DatagramPacket
用UDP编写客户/服务器程序时,无论是客户方还是服务器方,首先都要建立一个DatagramSocket对象用来接收或发送数据报,然后使用DatagramPacket类对象作为传输数据的载体。
4.2.1 数据报通信实例
Server:
public class UDPServerDemo {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket(9090);
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, 1024);
socket.receive(packet); //阻塞
System.out.println("Client address:"+packet.getSocketAddress());
System.out.println("Client data:"+new String(packet.getData(),0,packet.getLength()));
String str = "welcome";
//从接收到的数据报取得对方的地址和端口
DatagramPacket spacket = new DatagramPacket(str.getBytes(), str.getBytes().length, packet.getAddress(), packet.getPort());
socket.send(spacket);
socket.close();
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Client:
public class UDPClientDemo {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket(9091);
String str = "hello server";
DatagramPacket spacket = new DatagramPacket(str.getBytes(), str.getBytes().length,InetAddress.getByName("localhost"),9090);
socket.send(spacket);
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, 1024);
socket.receive(packet); //阻塞
System.out.println(packet.getSocketAddress());
System.out.println(new String(packet.getData(),0,packet.getLength()));
socket.close();
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
4.2.2 基于线程实现一对一通信
UDPPeerDemo:
class UDPReceiveThread implements Runnable
{
private DatagramSocket socket;
public UDPReceiveThread(DatagramSocket socket) {
this.socket = socket;
}
@Override
public void run() {
byte [] buffer = new byte[1024];
DatagramPacket p = null;
while( true ) {
p = new DatagramPacket(buffer, 1024);
try {
socket.receive(p);
System.out.println("Data: " + new String(p.getData(),0,p.getLength()));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
}
}
public class UDPPeerDemo implements Runnable
{
private DatagramSocket socket;
private int port;
private int rport;
public UDPPeerDemo(int port,int rport) {
this.port = port;
this.rport = rport;
try {
socket = new DatagramSocket(port);
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("套接字创建失败");
}
}
@Override
public void run() {
new Thread(new UDPReceiveThread(socket)).start(); //启动接收线程
String s = "";
Scanner scanner = new Scanner(System.in);
DatagramPacket packet = null;
while( scanner.hasNextLine() ) {
s = scanner.nextLine();
try {
packet = new DatagramPacket(s.getBytes(), s.getBytes().length,
InetAddress.getByName("localhost"), rport);
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
continue;
}
try {
socket.send(packet);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
socket.close();
}
}
UDPDemo:
public class UDPDemo {
public static void main(String[] args) {
new Thread(new UDPPeerDemo(9090, 9091)).start();
}
}
UDPDemo2:
public class UDPDemo2 {
public static void main(String[] args) {
new Thread(new UDPPeerDemo(9091, 9090)).start();
}
}
5、小结
通过网络资源定位器可以指向并获取网络上丰富的资源。基于TCP的网络数据传输是一种可靠的有连接的网络数据传输。这种传输方式是目前最常用的网络数据传输方式。在基于TCP的网络程序中,服务器端与客户端的程序编写稍有些不同。
基于UDP的网络数据传输是一种可靠的无连接的网络数据传输。在基于UDP的网络程序设计中,服务器端与客户端的程序编写基本上是相类似的。在Java的包java.net还含有很多用于网络数据传输的类和接口,为网络编程提供了方便。