💓博主CSDN主页:杭电码农-NEO💓
⏩专栏分类:Linux从入门到精通⏪
🚚代码仓库:NEO的学习日记🚚
🌹关注我🫵带你学更多操作系统知识
🔝🔝
Linux网络
- 1. 前言
- 2. 端口号详解
- 3. 认识TCP/UDP协议
- 4. 对网络字节序的理解
- 5. socket套接字API
- 6. 套接字编程
- 7. 总结
1. 前言
Linux网络部分,挺长时间没更新了, 秋招在即, 这篇文章就当是对网络知识的复习, 让我们一起进入网络的时间
本章重点:
本篇文章会认识, 端口号, 网络字节序等网络编程中的基本概念. 会带大家初识TCP和UDP协议. 并且通过套接字编程, 带大家实现一个简单的TCP/UDP服务器
2. 端口号详解
我们知道,一台机器可以启动多个服务. 那么当客户端拿到IP地址来访问你机器时, 你机器上有这么多个服务, 我怎么知道客户端想要访问哪个? 所以说我们需要一个字段来标识一台机器上的唯一一个服务(进程)
端口号(port)就是用来表示唯一一个服务的
IP + port可以标识全网唯一的服务
端口号(port)是传输层协议的内容:
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用.
端口号和进程pid的关系:
在学习Linux系统时,学到过进程id可以用来标识唯一的一个进程. 那么为什么网络服务不直接使用pid来标识唯一的服务(进程)呢? 答案是: 1. 一个端口号只能绑定一个进程, 但一个进程可以绑定到多个端口号上 2. 解耦合, 端口号是传输层到应用层寻找服务时需要使用的字段, 而进程的pid往往用于操作系统管理不同的进程
3. 认识TCP/UDP协议
我们先对TCP和UDP有一个大概的认识,在后面再详细讲解它的协议内容:
TCP协议:
- 传输层协议
- 有连接: 通信前需要先建立连接
- 可靠传输: TCP协议有一些措施来保证传输的可靠性
- 面向字节流
UDP协议:
- 传输层协议
- 无连接: 通信前不需要建立连接
- 不可靠传输: 无措施来保证可靠性
- 面向数据报
对于连接性的解释:
TCP通信前需要先建立连接(也就是大名鼎鼎的TCP三次握手,后面会讲). 而UDP通信什么都不用提前做. 我们可以把TCP通信比喻为打电话, 想和你通信必须先经过你的同意. 而把UDP通信比喻为寄快递, 我不需要经过你的同意,我只需要知道你的地址就可以无脑给你寄快递
对于可靠性的解释:
TCP是可靠的,而UDP是不可靠的, 那么在实际生活中我们用TCP就行了啊,为什么还要有UDP协议的存在呢? 相信聪明的你也能想到, TCP的可靠性是通过一定的策略实现的, 所以一定就证明了TCP是比UDP要复杂的, 并且从效率上来说, TCP和UDP谁快谁慢还不一定. 所以在不同场景下, 用到的协议也不同. 比如微信发送信息时, 我们一定不希望消息在网络传输中丢失了, 所以大概率会选择TCP协议. 而当我们在直播上网课时, 及时有部分包丢失在网络中,也不会对整个直播有太大的影响, 所以这时往往会选择UDP协议
对于面向字节流/数据报的解释:
什么是面向字节流? 意思就是TCP协议在发送数据时, 不管一次性发送多少数据, 也不管数据一共要发送几次,它只关心能尽快的将数据从客户端发送到服务器. 所以说TCP在发送数据时, 完整的数据可能是: “abcdefg123456"但是第一次可能发送了"abcdefg1”,第二次再发送"234",再发送"56". 这都是不定的. 而UDP是面向数据报的, 它每次发送数据时, 会将完整的数据全部保存在一个报文中, 然后将这个数据报整体发送过去
所以TCP会有粘包问题,后面会讲
4. 对网络字节序的理解
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
可使用系统调用做大小端字节序的交换
5. socket套接字API
socket函数的作用是创建套接字, 套接字的本质是一个文件描述符, 这个套接字会在后续中起重要作用. 第二个函数bind, 它的作用是: 将本服务的IP和端口号绑定到操作系统内部,供外部来访问. 而最后三个函数: listen和accept是TCP通信中需要用到的, 它们表示: listen用于开始监听是否有请求到来. accept用于将到来的请求拿到内存当中做解析. 而connect函数用于发送TCP请求的一方调用,与服务器建立连接
socket函数详解:
-
第一个参数代表套接字的类型,是个宏. AF_INET代表ipv4,最常用
-
第二个参数代表通信的类型,是个宏. SOCK_DGRAM代表UDP. SOCK_STREAM代表TCP
-
第三个参数设置为0即可
后续的函数看后面的代码就能懂, 如有不懂,欢迎私信
6. 套接字编程
由于socket套接字编程比较复杂, 所以这里只提供TCP的示例:
服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define BUF_SIZE 1024
#define PORT 8080
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUF_SIZE] = {0};
const char *hello = "Hello from server";
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 等待客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 发送一些数据
write(new_socket, hello, strlen(hello));
// 读取客户端数据并回显
while ((read(new_socket, buffer, BUF_SIZE - 1)) > 0) {
// 发送数据回客户端
write(new_socket, buffer, strlen(buffer));
memset(buffer, 0, BUF_SIZE);
}
if (read(new_socket, buffer, 0) < 0) {
perror("read failed");
}
// 关闭连接
close(new_socket);
close(server_fd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define BUF_SIZE 1024
#define SERVER_IP "127.0.0.1" // 服务器IP地址,这里使用本地回环地址
#define SERVER_PORT 8080 // 服务器端口号,与服务器设置的端口一致
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUF_SIZE] = {0};
char *message = "Hello from client";
// 创建socket文件描述符
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
// 配置服务器地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid server address");
exit(EXIT_FAILURE);
}
// 连接到服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
// 接收服务器的欢迎消息
read(sockfd, buffer, BUF_SIZE - 1);
printf("Server: %s\n", buffer);
// 向服务器发送消息
write(sockfd, message, strlen(message));
printf("Client: %s\n", message);
// 读取服务器的响应
memset(buffer, 0, BUF_SIZE); // 清空缓冲区以便接收新数据
read(sockfd, buffer, BUF_SIZE - 1);
printf("Server echo: %s\n", buffer);
// 关闭连接
close(sockfd);
return 0;
}
7. 总结
网络套接字编程是掌握网络至关重要的一步, 学会了这个网络编程的流程和底层逻辑, 下次遇见其他语言封装好的网络编程函数,你甚至能想象到它底层是如何实现的. 所以学技能不能只学接口,要学底层原理和系统调用, 这些你看所有封装好了的函数都是透明的