文章目录
博主个人社区:开发与算法学习社区
博主个人主页:Killing Vibe的博客
欢迎大家加入,一起交流学习~~
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
Socket是站在应用层,做网络编程很重要的一个概念
传输层、网络层、数据链路层、物理层 都是通过OS+硬件来提供服务的,而应用层要享受OS提供的网络服务,需要通过OS提供的服务窗口(Socket)来享受服务。
拓展:
OS原生的提供的系统调用(Linux上的网络编程):
int fd = socket();
setsocketopt(fd,TCP or UDP)
Socket套接字主要针对传输层协议划分为如下三类:
TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
以下为TCP的特点:
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
以下为UDP的特点:
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。
java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:

DatagramSocket是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
注意:
UDP服务器(Server):采用一个固定端口,方便客户端(Client)进行通信;
使用 DatagramSocket(int port) ,就可以绑定到本机指定的端口,此方法可能有错误风险,提示该端口已经被其他进程占用。
UDP客户端(Client):不需要采用固定端口(也可以用固定端口),采用随机端口;
使用 DatagramSocket() ,绑定到本机任意一个随机端口
DatagramSocket 普通方法(属于DatagramSocket类):
注意:
一旦通信双方逻辑意义上有了通信线路,双方地位就平等了(谁都可以作为发送方和接收方)
发送方调用的就是 send() 方法,接收方调用的就是 receive() 方法
通信结束后,双方都应该调用 close() 方法进行资源回收
DatagramPacket是UDP Socket发送和接收的数据报。
这个类就是定义的报文包:通信过程中的数据抽象
可以理解为:发送/接受的一个信封(五元组+信件)
DatagramPacket 构造方法:
注意:

DatagramPacket 普通方法:
注意:
getAddress() 方法和 getPort() 方法,用来获取客户端的ip地址和端口号port getData() ,用来拿到“信”(对方进程发送的应用层数据)InetSocketAddress ( SocketAddress 的子类 )构造方法:

以下仅展示部分代码,完整代码可以看博主的gitee仓库:
UDP客户端:
public class UserInputLoopClient {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
// 1. 创建 UDP socket
Log.println("准备创建 UDP socket");
DatagramSocket socket = new DatagramSocket();
Log.println("UDP socket 创建结束");
System.out.print("请输入英文单词: ");
while (scanner.hasNextLine()) {
// 2. 发送请求
String engWord = scanner.nextLine();
Log.println("英文单词是: " + engWord);
String request = engWord;
byte[] bytes = request.getBytes("UTF-8");
// 手动构造服务器的地址
// 现在,服务器和客户端在同一台主机上,所以,使用 127.0.0.1 (环回地址 loopback address)
// 端口使用 TranslateServer.PORT(8888)
InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
InetAddress remoteAddress = Inet4Address.getByName("182.254.132.183");
DatagramPacket sentPacket = new DatagramPacket(
bytes, 0, bytes.length, // 要发送的数据
remoteAddress, TranslateServer.PORT // 对方的 ip + port
);
Log.println("准备发送请求");
socket.send(sentPacket);
Log.println("请求发送结束");
// 3. 接收响应
byte[] buf = new byte[1024];
DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);
Log.println("准备接收响应");
socket.receive(receivedPacket);
Log.println("响应接收接收");
byte[] data = receivedPacket.getData();
int len = receivedPacket.getLength();
String response = new String(data, 0, len, "UTF-8");
String chiWord = response;
Log.println("翻译结果: " + chiWord);
System.out.print("请输入英文单词: ");
}
// 4. 关闭 socket
socket.close();
}
}
UDP服务端:
// 提供翻译的服务器
public class TranslateServer {
// 公开的 ip 地址:就看进程工作在哪个 ip 上
// 公开的 port:需要程序中指定
public static final int PORT = 8888;
// SocketException -> IOException -> Exception
public static void main(String[] args) throws Exception {
Log.println("准备进行字典的初始化");
initMap();
Log.println("完成字典的初始化");
Log.println("准备创建 UDP socket,端口是 " + PORT);
DatagramSocket socket = new DatagramSocket(PORT);
Log.println("UDP socket 创建成功");
// 作为服务器,是被动的,循环的进行请求-响应周期的处理
// 等待请求,处理并发送响应,直到永远
while (true) {
// 1. 接收请求
byte[] buf = new byte[1024]; // 1024 代表我们最大接收的数据大小(字节)
DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);
Log.println("准备好接收 DatagramPacket,最大大小为: " + buf.length);
Log.println("开始接收请求");
socket.receive(receivedPacket); // 这个方法就会阻塞(程序执行到这里就不动了,直到有客户发来请求,才能继续)
Log.println("接收到请求");
// 2. 一旦走到此处,一定是接收到请求了,拆信
// 拆出对方的 ip 地址
InetAddress address = receivedPacket.getAddress();
Log.println("对方的 IP 地址: " + address);
// 拆出对方的端口
int port = receivedPacket.getPort();
Log.println("对方的 port: " + port);
// 拆出对方的 ip 地址 + port
SocketAddress socketAddress = receivedPacket.getSocketAddress();
Log.println("对象的完整地址: " + socketAddress);
// 拆出对方发送过来的数据,其实这个 data 就是我们刚才定义的 buf 数组
byte[] data = receivedPacket.getData();
Log.println("接收到的对象的数据: " + Arrays.toString(data));
// 拆出接收到的数据的大小(字节)
int length = receivedPacket.getLength();
Log.println("接收的数据大小(字节):" + length);
// 3. 解析请求 :意味着我们需要定义自己的应用层协议
// 首先,做字符集解码 byte[] -> String
String request = new String(data, 0, length, "UTF-8");
// 这个按照我们的应用层协议
String engWord = request;
Log.println("请求(英文单词):" + engWord);
// 4. 执行业务(翻译服务),不是我们本次演示的重点
String chiWord = translate(engWord);
Log.println("翻译后的结果:" + chiWord);
// 5. 按照应用层协议,封装响应
String response = chiWord;
// 进行字符集编码 String -> byte[]
byte[] sendBuf = response.getBytes("UTF-8");
// 6. 发送响应
// 作为发送方需要提供
DatagramPacket sentPacket = new DatagramPacket(
sendBuf, 0, sendBuf.length, // 要发送的数据
socketAddress // 从请求信封中拆出来的对象的地址(ip + port)
);
Log.println("准备好发送 DatagramPacket 并发送");
socket.send(sentPacket);
Log.println("发送成功");
// 7. 本次请求-响应周期完成,继续下一次请求-响应周期
}
// socket.close(); // 由于我们是死循环,这里永远不会走到
}
private static final HashMap<String, String> map = new HashMap<>();
private static void initMap() {
map.put("apple", "苹果");
map.put("pear", "梨");
map.put("orange", "橙子");
}
private static String translate(String engWord) {
String chiWord = map.getOrDefault(engWord, "查无此单词");
return chiWord;
}
}
自定义的日志类(记得导入此类):
public class Log {
public static void println(Object o) {
LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String now = formatter.format(localDateTime);
String message = now + ": " + (o == null ? "null" : o.toString());
System.out.println(message);
}
public static void main(String[] args) {
println(1);
}
}

ServerSocket是创建TCP服务端Socket的API。
ServerSocket 构造方法:
服务器使用的TCP Socket对象(传入的端口,就是要公开的端口,一般称为监听(listen)端口)
ServerSocket 普通方法
注意:
accept:接起电话(服务器是电话铃响的这一方)
Socket对象:建立起的连接
close:挂电话(谁都可以挂)
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
注意:
Socket 普通方法:
注意:
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
对比以上长短连接,两者区别如下:
扩展了解:
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。
实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
现在还遗留一个问题:
如果同时多个长连接客户端,连接该服务器,能否正常处理?
需要在IDEA配置客户端支持同时运行多个实例!
所以可以使用多线程解决长连接客户端不支持同时在线的问题:
将任务专门交给其他线程来处理,主线程只负责接受socket。
这里仅演示短连接,长连接和多线程在博主的个人仓库下:
TCP服务端:
public class TranslateServerShortConnection {
public static final int PORT = 8888;
public static void main(String[] args) throws Exception {
Log.println("启动短连接版本的 TCP 服务器");
initMap();
ServerSocket serverSocket = new ServerSocket(PORT);
while (true) {
// 接电话
Log.println("等待对方来连接");
Socket socket = serverSocket.accept();
Log.println("有客户端连接上来了");
// 对方信息:
InetAddress inetAddress = socket.getInetAddress(); // ip
Log.println("对方的 ip: " + inetAddress);
int port = socket.getPort(); // port
Log.println("对方的 port: " + port);
SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress(); // ip + port
Log.println("对方的 ip + port: " + remoteSocketAddress);
// 读取请求
InputStream inputStream = socket.getInputStream();
Scanner scanner = new Scanner(inputStream, "UTF-8");
String request = scanner.nextLine(); // nextLine() 就会去掉换行符
String engWord = request;
Log.println("英文: " + engWord);
// 翻译
String chiWord = translate(engWord);
Log.println("中文: " + chiWord);
// 发送响应
String response = chiWord; // TODO: 响应的单词中是没有 \r\n
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8");
PrintWriter writer = new PrintWriter(outputStreamWriter);
Log.println("准备发送");
writer.printf("%s\r\n", response);
writer.flush();
Log.println("发送成功");
// 挂掉电话
socket.close();
Log.println("挂断电话");
}
// serverSocket.close();
}
private static final HashMap<String, String> map = new HashMap<>();
private static void initMap() {
map.put("apple", "苹果");
map.put("pear", "梨");
map.put("orange", "橙子");
}
private static String translate(String engWord) {
String chiWord = map.getOrDefault(engWord, "查无此单词");
return chiWord;
}
}
TCP客户端:
public class UserInputLoopShortConnectionClient {
public static void main(String[] args) throws Exception {
Scanner userInputScanner = new Scanner(System.in);
while (true) {
// 这里做了一个假设:1)用户肯定有输入 2)用户一行一定只输入一个单词(没有空格)
System.out.print("请输入英文单词: ");
if (!userInputScanner.hasNextLine()) {
break;
}
String engWord = userInputScanner.nextLine();
// 直接创建 Socket,使用服务器 IP + PORT
Log.println("准备创建 socket(TCP 连接)");
Socket socket = new Socket("127.0.0.1", TranslateServerShortConnection.PORT);
Log.println("socket(TCP 连接) 创建成功");
// 发送请求
Log.println("英文: " + engWord);
String request = engWord + "\r\n";
OutputStream os = socket.getOutputStream();
OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
PrintWriter writer = new PrintWriter(osWriter);
Log.println("发送请求中");
writer.print(request);
writer.flush();
Log.println("请求发送成功");
// 等待接受响应
InputStream is = socket.getInputStream();
Scanner socketScanner = new Scanner(is, "UTF-8");
// 由于我们的响应一定是一行,所以使用 nextLine() 进行读取即可
// nextLine() 返回的数据中,会自动把 \r\n 去掉
// TODO: 没有做 hasNextLine() 的判断
Log.println("准备读取响应");
String chiWord = socketScanner.nextLine();
Log.println("中文: " + chiWord);
socket.close();
}
}
}
byte[] buf = new byte[1024];
int n = inputStream.read(buf);
Scanner S = new Scanner(inputStream,"UTF-8");
s.nextLine() ... s.hasNextLine()
outputStream.write(buf,offset,length)OutputStreamWriter osWriter = new OutputStreamWriter(outputStream,"UTF-8");
PrintWriter writer = new PrintWriter(osWriter);
writer.println(...);
writer.print(...);
writer.printf(format,...);
重要:不要忘记刷新缓冲区,否则数据可能无法到达对方!!!
outputStream.flush();
//writer.flush();
举个栗子:


关于端口被占用的问题:
如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下:

此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用。以下为通过端口号查进程的方式:
在cmd输入 netstat -ano | findstr 端口号 ,则可以显示对应进程的pid。如以下命令显
示了8888进程的pid

在任务管理器中,通过pid查找进程

解决端口被占用的问题:
我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has
我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru
我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的
几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur
前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源
嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来