1. Socket
1.1. 本章目标
l 网络的基本概念
l InetAddress,InetSocketAddress的使用
l Tcp协议
l Socket编程
1.2. 网络
网络:将不同区域的计算机,遵从某种通信协议连接在一起。按照物理覆盖范围,可以分为局域网,城域网,(广域网)互联网。
如何区分每一台计算机? 通过ip地址,可以唯一的标示出一台计算机。
端口号:比方说你的计算机上的QQ如何和腾讯公司的QQ服务器做交互,通过ip地址+端口号,可以具体的区分出哪一台计算机上的qq。
1.3. Ip地址
IP地址是IP Address的缩写,全称是Internet Protocol Address,中文叫互联网协议地址或网际协议地址,是为了计算机网络相互连接进行通信而设计的协议。
IP地址类似于电话号码、身份证号,用于给网络上的电脑编号,一台电脑如果想联网,必须遵守IP协议。
IP地址由4个0-255之间的数字组成,在同一个网络内部,IP地址不能相同,所以可以用来标识网络上的一个设备,网络中我们只能依赖IP地址进行数据传输。
IPv4协议的32位地址,地址总容量近43亿个。而IPv6地址采用128位标识,数量为2的128次方。“IPv6可以让地球上每一粒沙子都拥有一个IP地址。”
美国现有IP地址约12亿个左右,平均每个美国人有4个IP地址。中国目前约5400万个,大约24个中国人1个IP地址。
如何查看自己的IP地址?
Windows:ipconfig
Ubuntu:ifconfig
0.0.0.0:表示本机
127.0.0.1:表示本机回环地址 localhost:本机地址
255:255:255:255 :表示当前自我,一般用于向当前子网广播信息
1.4. 端口
一台拥有IP地址的电脑可以提供多种服务,如网站、FTP服务,如果仅仅有一个IP地址,无法区分具体业务。显然不能只靠IP地址,实际上是通过“IP地址+端口号”来区分不同的服务的。
常见的服务对应的端口:
ftp:21,http:80,telnet:23,smtp:25,dns:53,https:443,tomcat:8080
一台电脑上最多只有65536(端口2个字节存储 16bit 2的16次方 =65536)个端口,每个端口对应一个程序,不同程序使用的端口不同。端口始终存在,但程序不一定启动。
注意:在写socket网络通信时,选取的端口号一般要选择1024以上, 1024以下给一些知名软件厂商预留。
1.5. 数据传输的方式有两种
TCP(Transfer Control Protocol) 传输控制协议方式,面向连接,三次握手,效率低。优点:稳定、可靠。缺点:建立连接和维持连接的代价高,传输速度不快。电话只需要拨号一次,就可以建立持久的通话,如果对方没听清你说的话,可以要求你重复,保证传输信息的可靠。
UDP(User Datagram Protocol) 用户数据报协议方式,不是面向连接,效率高。优点:开销小,传输速度快。缺点:传输方式不可靠,不稳定。类似于发短信,每次发送需要输入对方手机号,不是持久的连接,数据有可能丢失,系统只保证尽力发送。
在大型的网络编程中,会组合使用这两种方式实现数据的传递。
1.6. InetAddress,InetSocketAddress
例题1:
package com.net.address;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* java.net.InetAddress 此类表示互联网协议 (IP) 地址。
* static InetAddress getByName(String host)
在给定主机名的情况下确定主机的 IP 地址。
String getHostAddress()
返回 IP 地址字符串(以文本表现形式)。
String getHostName()
获取此 IP 地址的主机名。
* @author Administrator
*
*/
public class InetAddressDemo01 {
public static void main(String[] args) throws UnknownHostException {
System.out.println("----------获取本地InetAddress对象--------------");
//获取本地主机
InetAddress addr3 = InetAddress.getLocalHost();
//获取ip地址 10.0.2.98
System.out.println(addr3.getHostAddress());
//计算机名
System.out.println(addr3.getHostName());
System.out.println("----------根据域名获取InetAddress对象--------------");
//域名
//InetAddress addr2 = InetAddress.getByName("www.baidu.com");
//获取ip地址 118.34.20.100
//System.out.println(addr2.getHostAddress());
//获取的 www.baidu.com
//System.out.println(addr2.getHostName());
System.out.println("----------根据ip获取InetAddress对象--------------");
InetAddress addr = InetAddress.getByName("127.0.0.1");
//获取ip地址
System.out.println(addr.getHostAddress());
//如果addr中指定的ip地址存在,并且dns可以解析,那么此处获取的是ip地址对应的域名“WWW...com”
//如果addr中指定的ip地址不存在,或者dns不可以解析,那么此处获取的是原来的ip地址
System.out.println(addr.getHostName());
}
}
例题2:
package com.net.address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
/**
* InetSocketAddress 此类实现 IP 套接字地址(IP 地址 + 端口号)。 封装端口
* InetSocketAddress(String hostname, int port)
*
* InetSocketAddress(InetAddress addr, int port)
根据 IP 地址和端口号创建套接字地址。
根据主机名和端口号创建套接字地址。
* InetAddress getAddress()
获取 InetAddress。
String getHostName()
获取 hostname。
int getPort()
获取端口号。
* @author Administrator
*
*/
public class InetSocketAddressDemo01 {
public static void main(String[] args) throws UnknownHostException {
System.out.println("--------------构造1----------------");
InetSocketAddress addr = new InetSocketAddress("127.0.0.1",8888);
//InetSocketAddress addr = new InetSocketAddress("localhost",8888);
//获取主机名
System.out.println(addr.getHostName());
//获取端口号
System.out.println(addr.getPort());
System.out.println("--------------构造2----------------");
InetSocketAddress addr2 = new InetSocketAddress(InetAddress.getByName("127.0.0.1"),8999);
//获取InetAddress对象
InetAddress inadd = addr2.getAddress();
System.out.println("*****"+inadd.getHostName()+"*****"+inadd.getHostAddress());
//获取主机名
System.out.println(addr2.getHostName());
//获取端口号
System.out.println(addr2.getPort());
}
}
1.7. 什么是Socket
Socket编程是基于TCP/IP协议的网络编程,在两台电脑之间建立连接进行通讯,需要知道电脑的IP地址进行连接,这个连接的一端称为一个Socket。两台电脑之间的通信连接总是需要发起,不会凭空出现,发起连接的是客户端(Client),等待被连接的是服务端(Server),客户端服务端表示的是谁发起连接,与是否发送接收数据无关。
java在包java.net中提供了两个类Socket和ServerSocket,分别用来表客户端和服务端。客户端对象使用new Socket()创建,服务端对象使用new ServerSocket()创建。
1.8. 创建连接
服务端:发送读取 客户端:读取发送
分析: 服务器-à欢迎光临 客户端接收到 : 欢迎光临
客户端--à我来了 服务器接收到 :我来了
package com.net.socket01;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* Socket(String host, int port)
创建一个流套接字并将其连接到指定主机上的指定端口号。
Socket(InetAddress address, int port)
创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
* 指定ip地址:表明和哪台主机通信
* 指定端口号:表示和这台主机上的哪个程序通信(一个端口号,对应一个程序)
* @author Administrator
*
*/
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
//创建Socket对象,同时指定要连接的服务端的ip地址和端口号
// Socket client = new Socket("127.0.0.1",8023);
//localhost 表示本地地址 ,作用和127.0.0.1相同
// Socket client = new Socket("localhost",8023);
Socket client = new Socket(InetAddress.getByName("127.0.0.1"),8023);
//根据socket对象,创建输入流,读出服务端发送过来的信息
DataInputStream dis = new DataInputStream(client.getInputStream());
String msg = dis.readUTF();
System.out.println("客户端读取到的:"+msg);
//根据socket对象,创建输出流对象,用于给服务器发送信息
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
dos.writeUTF("我来了");
dos.flush();
}
}
package com.njwb.net.socket01;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
*
* @author Administrator
*
*/
public class Server {
public static void main(String[] args) throws IOException {
//创建ServerSocket对象
ServerSocket server = new ServerSocket(8023);
//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行
Socket socket = server.accept();
//如果代码能够执行到这里,表示已经和客户端建立连接了
//发送 欢迎光临 写出 输出流
//根据socket对象,构建输出流对象,用于向客户端写出数据
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF("欢迎光临");
dos.flush();
//根据socket对象,创建输入流读取,客户端发送过来的
DataInputStream dis = new DataInputStream(socket.getInputStream());
String msg = dis.readUTF();
System.out.println("服务端收到的:"+msg);
}
}
总结Socket通讯的过程
(1) 服务端创建socket监听某个端口是否有连接请求。
(2) 客户端创建socket连接该端口。
(3) 服务端Accept(接受)以后,一个连接建立成功。
(4) 双方根据socket创建输入流,输出流。
(5) 双方可以通过流互相发送信息。
1.9. 连接持久化
要求:客户端可以从键盘端循环输入(发送),并且可以接收服务端发送的,服务端必须循环读取客户端信息同时回发给客户端
package com.net.socket02;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* address already in use 出现这个异常,Console控制台开启的多余的Server关掉
* @author Administrator
*
*/
public class Server {
public static void main(String[] args) throws IOException {
//创建ServerSocket对象
ServerSocket server = new ServerSocket(8023);
//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行
Socket socket = server.accept();
//根据socket对象,创建输入流读取,客户端发送过来的
DataInputStream dis = new DataInputStream(socket.getInputStream());
//根据socket对象,构建输出流对象,用于向客户端写出数据
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
while(true){
String msg = dis.readUTF();
System.out.println("服务端收到的:"+msg);
dos.writeUTF(msg);
dos.flush();
}
}
}
package com.net.socket02;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* 需要循环输入
* 客户端:先发送 ,再读取
* 需要循环转发
* 服务端:先读取,再发送
* 客户端:
* 1.创建客户端的socket对象 ,同时指定连接的服务端的ip地址和端口号
* 2.根据socket对象,创建输入,输出流
* 3.根据流,先发送(键盘端输入的),再读取
* 4.关闭流(暂时不关)
* 服务端:
* 1.服务端创建ServerSocket对象,同时指定端口号
* 2.服务端等待客户端触发通信 accept
* 3.根据socket对象,创建输入,输出流
* 4.转发 先读,再发
* 5.关闭流(暂时不关)
* @author Administrator
*
*/
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
//创建Socket对象,同时指定要连接的服务端的ip地址和端口号
Socket client = new Socket("127.0.0.1",8023);
//声明键盘端输入的实例变量
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
//根据socket对象,创建输出流对象,用于给服务器发送信息
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
//根据socket对象,创建输入流,读出服务端发送过来的信息
DataInputStream dis = new DataInputStream(client.getInputStream());
while(true){
//键盘端输入的变量,存储在msg中,将msg发送出去
String msg = console.readLine();
dos.writeUTF(msg);
dos.flush();
String data = dis.readUTF();
System.out.println("客户端读取到的:"+data);
}
}
}
1.10. 双工客户端
半双工是指一个时间段内只有一个动作发生。一条窄窄的马路,同时只能有一辆车通过,当目前有两辆车对开,这种情况下就只能一辆先过,等到头后另一辆再开。早期对讲机就是半双工。
全双工的系统允许二台设备间同时进行双向资料传输。一般的电话、手机就是全双工的系统,因为在讲话时同时也可以听到对方的声音。
现在的问题是,客户端必须先发送,才能接收?
实际的聊天室,发送和读取相互分离的,互相不影响。
如何实现客户端多线程?
要求:客户端必须实现发送和接收2个线程(发送和接收分离),发送线程实现键盘端输入并发送,接收线程接收消息并输出。服务端依然循环处理,把接收到消息,回传给客户端。
package com.net.socket03;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* 发送线程
* @author Administrator
*
*/
public class Send implements Runnable {
private DataOutputStream dos; //声明输出流对象,用于发送消息
private BufferedReader console; //声明键盘端输入的实例变量
private Socket socket; //声明socket,用于构建输出流对象
private boolean isRunning=true; //用于控制循环是否继续
/**
* 往线程传递数据,此处从client类传入socket对象,用于构建输出流
* @param socket
*/
public Send(Socket socket) {
super();
this.socket = socket;
console = new BufferedReader(new InputStreamReader(System.in));
try {
//根据传入的socket,构建了输出流对象
dos = new DataOutputStream(this.socket.getOutputStream());
} catch (IOException e) {
isRunning=false;
e.printStackTrace();
}
}
@Override
public void run() {
while(isRunning){
try {
String msg = console.readLine();
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
isRunning=false;
e.printStackTrace();
}
}
}
}
package com.njwb.net.socket03;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* 接收线程
* @author Administrator
*
*/
public class Receive implements Runnable{
private DataInputStream dis; //声明输入流对象,用于读取服务端发送过来的
private Socket socket; //声明socket对象
private boolean isRunning=true; //声明boolean变量,用于控制循环是否继续
public Receive(Socket socket) {
super();
this.socket = socket;
//根据传入的socket对象,构建输入流对象
try {
dis = new DataInputStream(this.socket.getInputStream());
} catch (IOException e) {
isRunning=false;
e.printStackTrace();
}
}
@Override
public void run() {
while(isRunning){
try {
String msg = dis.readUTF();
System.out.println("客户端收到的:"+msg);
} catch (IOException e) {
isRunning=false;
e.printStackTrace();
}
}
}
}
package com.njwb.net.socket03;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* 现在的是单线程的,都写在main线程里,代码写出 必须先write()发送出去,才能read()读取进来,导致发送,读取不能相互独立。
* 有必要,拆分成2个线程,发送线程,读取(接收)线程
* 1.双工客户端 send发送线程 : 输出流 dos,键盘接收的变量consle, 声明了Socket对象,从Client类将Scoket对象传入Send类 ,循环发送的变量控制 isRunning,
* run线程体中,不写while(),只能发送1句
* receive接收线程: 输入流对象dis ,socket对象 ,isRunning
* @author Administrator
*
*/
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
//创建Socket对象,同时指定要连接的服务端的ip地址和端口号
Socket client = new Socket("127.0.0.1",8023);
//开启发送线程
new Thread(new Send(client),"发送线程").start();
//开启接收线程
new Thread(new Receive(client),"接收线程").start();
}
}
1.11. 服务端并发接收多个客户端的连接
当前的问题
服务端只能与一个客户端连接,谁先到谁先聊,后来的无法接收。
服务端如何应付多个客户端的同时连接?
服务端使用多线程,每次有客户端接入时,创建一个服务端线程与客户端对接,每个服务端线程负责一个客户端。此时服务器端,必须循环实现创建服务端线程,并且服务端线程类必须实现读出的内容回传给客户端
如何实现服务端多线程?
package com.net.socket04;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 现在的问题,启动1个server后,如果开启多个client,发现谁先到,谁先聊,服务端只能支持1个客户端,即使你开启了多个客户端
* 解决办法1:将 server.accept()放到循环中,理论上可以接收多个客户端,发现仍旧解决不了问题,
* 解决办法2:服务端开启多线程,来一个客户端,开启一个多线程类(10086客服)接待一下
* 分析: 转发 先读取,在发送 DataInputStream dis 输入流对象,DataOutputStream dos ,输入流,输出流需要根据什么构建,
* socket对象 ,socket对象通过带参构造传入 ,可行,能够实现1对多
* @author Administrator
*
*/
public class Server {
public static void main(String[] args) throws IOException {
//创建ServerSocket对象
ServerSocket server = new ServerSocket(8023);
while(true){
//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行
Socket socket = server.accept();
System.out.println("看,有1个客户端过来了");
//根据socket对象,创建输入流读取,客户端发送过来的
DataInputStream dis = new DataInputStream(socket.getInputStream());
//根据socket对象,构建输出流对象,用于向客户端写出数据
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
while(true){
String msg = dis.readUTF();
System.out.println("服务端收到的:"+msg);
dos.writeUTF(msg);
dos.flush();
}
}
}
}
package com.njwb.net.socket04;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 现在的问题,启动1个server后,如果开启多个client,发现谁先到,谁先聊,服务端只能支持1个客户端,即使你开启了多个客户端
* 解决办法1:将 server.accept()放到循环中,理论上可以接收多个客户端,发现仍旧解决不了问题,
* 解决办法2:服务端开启多线程,来一个客户端,开启一个多线程类(10086客服)接待一下
* 分析: 转发 先读取,在发送 DataInputStream dis 输入流对象,DataOutputStream dos ,输入流,输出流需要根据什么构建,
* socket对象 ,socket对象通过带参构造传入
* @author Administrator
*
*/
public class Server2 {
public static void main(String[] args) throws IOException {
//创建ServerSocket对象
ServerSocket server = new ServerSocket(8023);
while(true){
//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行
Socket socket = server.accept();
System.out.println("看,有1个客户端过来了,开启一个客服(多线程类ServerChannel)接待一下");
ServerChannel chan = new ServerChannel(socket);
new Thread(chan,"客服角色").start();
}
}
}
package com.njwb.net.socket04;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ServerChannel implements Runnable{
private DataInputStream dis;
private DataOutputStream dos;
private Socket socket;
private boolean isRunning=true;
public ServerChannel(Socket socket) {
super();
this.socket = socket;
try {
//根据传入的socket对象,构建输入流对象,用于读取
dis =new DataInputStream(this.socket.getInputStream());
//根据传入的socket对象,构建输出流对象,用于写出
dos = new DataOutputStream(this.socket.getOutputStream());
} catch (IOException e) {
isRunning=false;
e.printStackTrace();
}
}
@Override
public void run() {
//先读取,再发送
while(isRunning){
try {
String msg = dis.readUTF();
System.out.println("服务端读取的:"+msg);
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
isRunning=false;
e.printStackTrace();
}
}
}
}
1.12. 群聊
服务端将数据分发给所有连接的客户端
要求:服务端必须有保存处理客户端信息的服务端线程类的集合.发送数据时,除自身外,其他客户端都应该收到。每个服务端线程类对应一个客户端。
package com.njwb.net.socket06;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 1.3个客户端
* 客户端1:
* 启动后 请输入姓名:jack
* 系统消息:jack,欢迎你进入。
* 系统消息:tom,已经进入了聊天室
* 系统消息:helen,已经进入了聊天室
* @helen: hello,你在做什么
* 今天吃了10个包子
* 客户端2:
* 请输入姓名 tom
* 系统消息:tom,欢迎你进入
* 系统消息:helen,已经进入了聊天室
* jack对大家说:今天吃了10个包子
* 客户端3:
* 请输入姓名 helen
* 系统消息:helen,欢迎你进入
* jack悄悄对你说: hello,你在做什么
* jack对大家说:今天吃了10个包子
* 支持私聊 如果发出的格式 @XXX: 只有 XXX能收到消息,其他人收不到
*
* 服务端:存储聊天记录 ip地址 + “ ”+客户端姓名: + “ ”+ 年-月-日 时:分:秒: 聊天内容 ,按行存储 ,存储到
* E:/others/聊天内容.txt中,追加
* 服务端: E:/others/关键词.txt, 里面有多行关键词,一行一个 ,“笨蛋”,“白痴”,“傻瓜”.....
* 如果发现要转发的内容中包含了关键词,这个时候,服务端提醒该发送者 :你发送的内容中有侮辱性词语,已被屏蔽。服务器不做转发。
*
* @author Administrator
*
*/
public class Server {
private List<ServerChannel> clist = new ArrayList<ServerChannel>();
public static void main(String[] args) throws IOException {
//对象名.方法名()
new Server().start();
// Server s = new Server();
// s.start();
}
public void start() throws IOException{
//创建服务端ServerSocket
ServerSocket server = new ServerSocket(8023);
while(true){
Socket socket = server.accept();
System.out.println("看, 有1个客户端过来了,开启一个客服接待一下,该客户端的ip地址:"+socket.getInetAddress().getHostAddress());
ServerChannel chan = new ServerChannel(socket);
//加入集合,方便后面的遍历
clist.add(chan);
new Thread(chan,"客服").start();
}
}
//设计成内部类,仍旧转发
public class ServerChannel implements Runnable{
private DataInputStream dis;
private DataOutputStream dos;
private Socket socket;
private boolean isRunning=true;
/**
* 通过带参构造传入socket对象,用于构建输入,输出流
* @param socket
*/
public ServerChannel(Socket socket) {
super();
this.socket = socket;
try {
dis = new DataInputStream(this.socket.getInputStream());
dos = new DataOutputStream(this.socket.getOutputStream());
//检索名字
//给自身发送欢迎消息
//给其他的已经登录的发送系统消息
} catch (IOException e) {
isRunning=false;
e.printStackTrace();
}
}
@Override
public void run() {
while(isRunning){
try {
//读取内容
String msg = dis.readUTF();
System.out.println("服务端收到的:"+msg);
//将读取的内容转发给其他客户端
for(ServerChannel chan:clist){
//屏蔽自身,除了自身不发送
if(chan!=this){
//chan :表示的是其他客户端对应的客服(多线程类ServerChannel)
chan.dos.writeUTF(msg);
chan.dos.flush();
}
}
} catch (IOException e) {
isRunning=false;
//删除出错的客户端
for(ServerChannel chan:clist){
if(chan==this){
clist.remove(chan);
}
}
e.printStackTrace();
}
}
}
}
}