在拨号上网的时代,上网被看作一个通过与“互联网”这位朋友打电话的行为。这种信息建的交互形成网络,再按照一定规则协议,形成了套接字(Socket)。
早期计算机技术发展过程中,很多术语来自于英文,译者在寻找合适的中文术语时,会结合字面意义和技术特性,偶尔有时会采用音译(ex.鲁棒性robustness)。如果将Socket去翻译软件翻译,得到的会是插座,代表的意思接近一个链接点或接口。根据几个字拆开来再与直译对比来看:
“套”有包围、套住含义,“接”有链接的含义,“字”,是一种计算机的单位。
这样组合起来,就表达了这个socket背后的含义,即使表面有点拗口。
现如今,更多接受的翻译方式是不翻译。
到这里可能还是一个模糊的概念,用一个从安装电话到打电话(沟通)的过程来解释这个概念。
// 该函数用于创建一个套接字
extern int __sys_socket(int family, int type, int protocol);
// 该函数用于将套接字绑定到一个地址上
extern int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen);
// 监听指定套接字的连接请求
extern int __sys_listen(int fd, int backlog);
// 该函数用于接受一个传入的连接请求。
extern int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr, int __user *upeer_addrlen, int flags);
如果说在 Java 中,万物皆对象,那么在Linux中可以说万物皆文件。
Socket 也是一种文件,所以 Linux 在网络传输的过程中可以使用文件I/O相关的函数。
// sys_close函数用于关闭一个已打开的文件描述符。
// 参数: fd - 要关闭的文件描述符。文件,Socket,Pipe都可以传入
// 返回值: 成功时返回0,失败时返回-1,并设置errno。
int sys_close(int fd)
在Linux中创建一个Socket,通过下面的方法实现
// @param family 套接字地址族,如AF_INET表示IPv4
// @param type 套接字类型,如SOCK_STREAM表示TCP流式套接字
// @param protocol 使用的协议,通常为0,系统将自动选择合适的协议
// @return 成功时返回新创建的套接字文件描述符,失败时返回-1并设置errno
int __sys_socket(int family, int type, int protocol);
这里和font family类似,可以理解为“族”,你可以在linux/include/sockect.h
找到支持的全部协议。
/* Supported address families. */
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol 常说的IPV4 */
#define AF_AX25 3 /* Amateur Radio AX.25 */
#define AF_IPX 4 /* Novell IPX */
#define AF_APPLETALK 5 /* AppleTalk DDP */
#define AF_NETROM 6 /* Amateur Radio NET/ROM */
#define AF_BRIDGE 7 /* Multiprotocol bridge */
省略...
类型,相关定义在include/linux/net.h
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
第一种流式,比较常见。第二种源码的注释这样描述datagram (conn.less) socket
这二者的关系就像是TCP和UDP。再后面的就是根据不同场景和协议层的不同类型,有的面相传递传递顺序做优化,SOCK_RDM。有的则更多用于自定义,SOCK_RAW。
协议,和地址族支持一致。
Protocol families, same as address families.
知道了以上这些基础,则可以创建一个简单的TCP Socket
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0)
但如果想要“打电话”,还需要接一根电话线,知道对方“号码”。
当启动一个Spring Boot后,你会熟练的打开127.0.0.1:8080来查看一下是否正常。如果偷懒不想输入端口号,会在application.yml里设置:
server:
port: 80
前面的一串数字就是IP,冒号后面的就是端口号。我们创建的socket也需要分配这样一个“组合”。
有很长一段时间里,不明真相的我仍认为IP + Port = Socket。
客户端(client)与服务端(server),能分得这么清晰的时候,通常是用浏览器访问一个网站,此时浏览器叫做客户端,被访问的网站就是服务端。
还有一个词容易混淆,他就是终端。终端来自于他属于最边缘的设备,客户端通常是终端上的软件。你的手机,电脑就是终端,上面运行的浏览器便是客户端。
如果是两个人在局域网聊天,那双方各自为client和server。所以另一台机器也需要去创建一个Socket且分配IP和Port。
# 绑定到一个IP地址和端口号
server_address = ('0.0.0.0', 8080) # 监听所有可用的网络接口上的8080端口
socket.bind(server_address)
三次握手,Three-Way Handshake。
及时双方各自为client和server,那在一次请求时,也有发送和接收。发送和接收都有失败的概率,为了收到了你的收到,为了不“黑暗森林”,双方以这样一种形式确定了对接成功。
收发之前,记得先让Socket开始监听
listen(socket, 8888)
下面是一个模拟的过程
1.客户端发送 SYN 包:
Client -> Server: SYN (ISN = x)
2. 服务器发送 SYN+ACK 包:
Server -> Client: SYN (ISN = y), ACK (x + 1)
3.客户端发送 ACK 包:
Client -> Server: ACK (y + 1)
在这个过程中,每个数据包都包含以下信息:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
fd_set readfds;
// 创建Socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 绑定地址
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_id, MAX_CLIENTS) < 0) {
perror("listen");
exit(EXITTargetException);
}
printf("Chat server started on port 8080\n");
while (1) {
// 清空文件描述符集合
FD_ZERO(&readfds);
// 添加服务器Socket到集合
FD_SET(server_fd, &readfds);
int max_sd = server_fd;
// 添加客户端Socket到集合
for (int i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i];
if (sd > 0) {
FD_SET(sd, &readfds);
}
if (sd > max_sd) {
max_sd = sd;
}
}
// 监听所有Socket
if (select(max_sd + 1, &readfds, NULL, NULL, NULL) < 0) {
perror("select error");
exit(EXIT_FAILURE);
}
// 处理新的连接
if (FD_ISSET(server_fd, &readfds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("New connection, socket fd is %d, ip is : %s, port : %d\n", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 将新连接添加到客户端Socket数组
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == 0) {
client_sides[i] = new_socket;
break;
}
}
}
// 处理客户端消息
for (int i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i];
if (FD_ISSET(sd, &readfds)) {
if (read(sd, buffer, BUFFER_SIZE) == 0) {
// 客户端断开连接
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
printf("Host disconnected, ip %s, port %d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
close(sd);
client_sockets[i] = 0;
} else {
// 广播消息给所有客户端
buffer[BUFFER_SIZE - 1] = '\0';
printf("Received message: %s\n", buffer);
for (int j = 0; j < MAX_CLIENTS; j++) {
int client_sd = client_sockets[j];
if (client_sd != 0 && client_sd != sd) {
send(client_sd, buffer, strlen(buffer), 0);
}
}
}
}
}
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_IDEA] = {0};
char *message = "Hello from client";
// 创建Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将IP地址转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(servai_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
// 发送消息到服务器
send(sock, message, strlen(message), 0);
// 接收服务器消息
int valread = read(sock, buffer, BUFFER_SIZE);
printf("\nServer: %s\n", buffer);
return 0;
}
如何优化性能?
从面试最烦人的三次握手开始优化。倘若三次握手还是没成功,会不断尝试,但时间会依次递增,所以可以设置一个三次重试后直接失败返回。
自定义协议。比如使用跳过TCP层的SOCK_RAW类型。(注意风险)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。