Socket

一种独立于协议的网络编程接口,所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。

Socket主要类型

流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。
数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。
原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。

Socket主要流程

要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket。

根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:
(1)服务器监听。
(2)客户端请求。
(3)连接确认 。

1.服务器监听
所谓服务器监听,是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。

2.客户端请求
所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端接字提出连接请求。

3.连接确认
所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求。

Socket主要特点

根据套接字的不同类型,可以将套接字调用分为面向连接服务和无连接服务。

面向连接服务的主要特点如下:
(1)数据传输过程必须经过建立连接、维护连接和释放连接3个阶段 ;
(2)在传输过程中,各分组不需要携带目的主机的地址;
(3)可靠性好,但由于协议复杂,通信效率不高 。

面向无连接服务的主要特点如下:
(1)不需要连接的各个阶段 ;
(2)每个分组都携带完整的目的主机地址,在系统中独立传送 ;
(3)由于没有顺序控制,所以接收方的分组可能出现乱序、重复和丢失现象 ;
(4)通信效率高,但可靠性不能确保。

Socket的引入

为了更方便地开发网络应用程序,美国伯克利大学在UNIX上推出了一种应用程序访问通信协议的操作系统调用接字(Socket)。 Socket的出现,使得程序员可以很方便地访问 TCPIP,从而开发各种网络应用程序。后来套接字被引进到 Windows等操作系统,成为开发网络应用程序的有效工具。
套接字存在于通信区域,通信区域也被称为地址族,主要用于将通过套接字通信的进程的公有特性综合在一起。套接字通常只与同一区域的套接字交换数据。Windows Socket只支持一个通信区域——AF_INET国际网区域,使用网际协议族通信的进程使用该域。

Http与Socket的区别

在以前我们实现数据交换已经有了HTTP协议,为什么还要学习Socket?
回顾当输出 www.baidu.com 的时候浏览器执行了那些操作?

http通信的特点:

连接属于非持久性连接:TCP的三次握手
客户端只能访问服务端,服务端无法访问客户端,属于单项通信
TCP三次握手:
TCP三次握手过程中不传递数据,只为同步连接双方的序列号和确认号传递数据,在握手后服务端和客户端才开始传输数据,在理想状态下,TCP连接一旦建立,在通信的双方中任何一方主动断开连接之前TCP连接会一直保持下去。

Socket通信特点:

1、持久性连接
2、双向通信,客户端能访问服务端,服务端也能访问客户端

Socket是对TCP/IP协议的封装,Socket只是一个接口而不是一个协议,通过Socket我们才能使用TCP/IP/UDP协议。

Socket原理

Socket原理

开始写一个简单的Socket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//server.js
//服务器端

// 加载net模块
var net = require("net")
// 创建net实例对象
var server = net.createServer();

server.on("connection",function(client){
client.on("data",function(data){
console.log("客户端发来:"+data)
// 向客户端发送信息
client.write("测试");
})

// Socket客户端关闭
client.on('close', function (err) {
console.log(client.remotePort + '退出了聊天室');
}
})

// 建立监听
server.listen(3000, function () {
console.log('Starting Socket server...');
console.log('http://127.0.0.1:3000');
console.log('');
})

// 服务器异常
server.on('error', function () {
console.log('Server error');
});

client.js

//客户端
var net= require("net")

// 创建连接
var socket = net.createConnection({
host: '127.0.0.1',
port: 3000
})

socket.on('connect', function () {
// 一直读取输入
process.stdin.on('data', function (msg) {
socket.write(msg.toString();
});
}


// 接收服务器端发来的数据
socket.on("data",function(data){
console.log("服务器发来:"+data)
})

这是一个简单实例,也很好理解。

编写Chat-Cli

Chat-cli就是在命令行上聊天都一种方式,为什么要在命令行上聊天呢?因为这样的程序占用较小,可以边敲命令边聊天,这看起来是不是很不错?接下来我们将深入的编写Chat-cli,我将我编写的命名为NChat-Cli。
实现原理归根结底就是将所有加入的用户存在一个数组里面。
那放数组里面怎么知道那个是谁呢?我们可以用内置的一个函数Socket.remotePort获取对应的id,可以将名字加id结合起来找到对应用户,一个用户实质就是一个Socket,如果要实现私聊,可以将这些信息存储至数组里面,比如{username: username, id: id, socket: socket},通过id查找用户(Socket)
如何实现多房间聊天呢?
这个实现也很简单,还是吧用户存储在数组里面,可以创建一个二维数组Array[HomeID][UserSocket],也可以存储用户信息,可以这样Array[HomeID]{username: username, id: id, socket: socket},当用户向房间发送信息时,遍历这个房间所有用户,向遍历出来的用户逐一发送,除自己外。
上面提到的一些变量意思为: username:用户名,id:用户id,Socket和UserSocket:就是一个用户,HomeID:房间号。

1
2
npm init # 初始化一个项目
npm install moment --save # 安装时间工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//server.js
// 加载net模块
const net = require("net");
// 加载时间模块
var moment = require('moment');
// 创建net实例对象
var server = net.createServer();
// 保存所有客户的socket对象
var users = [];
// 保存所有房间ID
var homeIDs = Array();


// 计算数组总数,不计空数组
function count_array_num(array){
var num = 0;
for (let i = 0; i < array.length; i++) {
if(array[i] != undefined || array[i] != null){
num++;
}
}
return num;
}

// 建立监听
server.listen(3000, function () {
console.log('Starting NChat server...');
console.log('http://127.0.0.1:3000');
console.log('');
})


server.on('connection', function (socket) {
// 客户端发送的数据
var client_data;

// 将加入聊天室的用户加入至数组
users.push(socket);

socket.on('data', function (data) {
console.log(data.toString());
client_data = JSON.parse(data);

// 房间用户初始化
if(homeIDs[client_data.homeID] == undefined){
homeIDs[client_data.homeID] = Array();
}

// 昵称为空判断
if(client_data.username == null){
//为空则初始化昵称
client_data.username = "User" + socket.remotePort;
}

// 加入房间
var isExist = 0; // o: false , 1: true
for(var i = 0; i < homeIDs[client_data.homeID].length; i++){
// 用户加入判断
if(homeIDs[client_data.homeID][i] != undefined && homeIDs[client_data.homeID][i].remotePort == socket.remotePort){
isExist = 1;
break;
}
}

// 如果用户没有加入房间则加入
if (isExist == 0) {
homeIDs[client_data.homeID].push(socket);
var time = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
console.log(time + ' ' + client_data.username + '(' + socket.remotePort + ')' + ' Join ' + client_data.homeID + ' home(Online num: ' + count_array_num(homeIDs[client_data.homeID]) + ')');
for(var j = 0; j < homeIDs[client_data.homeID].length; j++){
if(homeIDs[client_data.homeID][j] != undefined && homeIDs[client_data.homeID][j] != socket){
homeIDs[client_data.homeID][j].write(time + ' ' + client_data.username + '(' + socket.remotePort + ')' + '加入了 ' + client_data.homeID + ' 房间(在线人数: ' + count_array_num(homeIDs[client_data.homeID]) + ')');

} else if(homeIDs[client_data.homeID][j] != undefined){
socket.write('加入 ' + client_data.homeID + ' 房间成功! 在线人数: ' + count_array_num(homeIDs[client_data.homeID]) + ')\n');
}
};
}

// 发送信息
for(var k = 0; k < homeIDs[client_data.homeID].length; k++){
if(homeIDs[client_data.homeID][k] != undefined && client_data.message != undefined && homeIDs[client_data.homeID][k] != socket){
var time = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
homeIDs[client_data.homeID][k].write(time + '\n' + client_data.username + ': ' + client_data.message);
}
}
})

// 用户退出调用
socket.on('close', function (err) {
var time = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
console.log(time + ' ' + client_data.username + '(' + socket.remotePort + ')' + ' Exit ' + client_data.homeID + ' home(Online num: ' + count_array_num(homeIDs[client_data.homeID]) + ')');
// 删除房间里面的退出去的用户
for(var j = 0; j < homeIDs[client_data.homeID].length; j++){
if(homeIDs[client_data.homeID][j] == socket){
delete homeIDs[client_data.homeID][j];
} else if(homeIDs[client_data.homeID][j] != undefined){
// 输出退出信息,不输出给退出的用户
homeIDs[client_data.homeID][j].write(time + ' ' + client_data.username + '(' + socket.remotePort + ')' + ' 退出了 ' + client_data.homeID + ' 房间(在线人数: ' + count_array_num(homeIDs[client_data.homeID]) + ')\n');
}
}

users.forEach(function (uesr) {
// 删除退出去的总用户
for(var k = 0; k < users.length; k++){
if( users[k] == socket){
delete users[k];
}
}
});
})
})

// 服务器异常
server.on('error', function () {
console.log('Server error');
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// client.js

//导入net
var net = require('net');

//发送数据的实体
var data = {
username: null,
homeID: null,
message: undefined
}

//处理参数
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
switch(argv[i]){
case '-n':
data.username = argv[i +1];
i++;
break;
case '-id':
data.homeID = argv[i +1];
i++;
break;
case '-c':
content = argv[i +1];
i++;
break;
case '-v':
console.log('NChat Version: 1.0 bate');
process.exit(0);
case '--help':
console.log('Usage: nchat [options]');
console.log('');
console.log('Options:');
console.log(' -n add username');
console.log(' -id add homeid');
console.log(' -c send content');
console.log(' -v get version');
console.log(' --help help');
process.exit(0);
default:
console.log("Unknown parameter");
}
}

// 创建连接
var socket = net.createConnection({
host: '127.0.0.1',
port: 3000
})

// 输入消息
socket.on('connect', function () {
// 待发送内容
var content;

//提前告诉服务端个人信息
socket.write(JSON.stringify(data));

if(content != null){
data.message = content;
socket.write(JSON.stringify(data));
process.exit(0);
}

//读取要发送给用户的内容
process.stdin.on('data', function (msg) {
process.stdout.write('\n');
data.message = msg.toString().slice(0, -1);
socket.write(JSON.stringify(data));
});
});

//接收消息
socket.on('data', function (data) {
console.log(data.toString() + '\n');
});
//服务器异常
socket.on('error', function (err) {
console.log('Server error');
})

编译运行

1
2
npm run start
node client.js -n 昵称 -n 房间号

然后就可以在命令行上面聊天啦!
开源地址 Github


本站由 Natuie 使用 Stellar 1.26.8 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站总访问量次 | 本站总访客数人次
载入天数...载入时分秒...