1. 前言
Linux网络部分,挺长时间没更新了, 秋招在即, 这篇文章就当是对网络知识的复习, 让我们一起进入网络的时间
2. 端口号详解
我们知道,一台机器可以启动多个服务. 那么当客户端拿到IP地址来访问你机器时, 你机器上有这么多个服务, 我怎么知道客户端想要访问哪个? 所以说我们需要一个字段来标识一台机器上的唯一一个服务(进程)
端口号(port)就是用来表示唯一一个服务的
IP + port可以标识全网唯一的服务
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用.
3. 认识TCP/UDP协议
我们先对TCP和UDP有一个大概的认识,在后面再详细讲解它的协议内容:
- 传输层协议
- 有连接: 通信前需要先建立连接
- 可靠传输: TCP协议有一些措施来保证传输的可靠性
- 面向字节流
- 传输层协议
- 无连接: 通信前不需要建立连接
- 不可靠传输: 无措施来保证可靠性
- 面向数据报
所以TCP会有粘包问题,后面会讲
4. 对网络字节序的理解
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
5. socket套接字API
-
第一个参数代表套接字的类型,是个宏. AF_INET代表ipv4,最常用
-
第二个参数代表通信的类型,是个宏. SOCK_DGRAM代表UDP. SOCK_STREAM代表TCP
-
第三个参数设置为0即可
后续的函数看后面的代码就能懂, 如有不懂,欢迎私信
6. 套接字编程
服务器
#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. 总结
网络套接字编程是掌握网络至关重要的一步, 学会了这个网络编程的流程和底层逻辑, 下次遇见其他语言封装好的网络编程函数,你甚至能想象到它底层是如何实现的. 所以学技能不能只学接口,要学底层原理和系统调用, 这些你看所有封装好了的函数都是透明的