什么是socket
一、Socket编程概述
Socket是网络通信的端点抽象,如同现实世界中的电话插座,一个人想要用电话,肯定需要把插头插到插座中,而在网络上你想跟其他节点通信,也需要将socket这个插头插进网络中。在Linux系统中,socket表现为一个int类型的文件描述符,我们完全可以把其当作一个文件,对其执行读取写入等操作。
二、从网络模型开始看socket
1. TCP/IP四层模型
层级 | 协议示例 | 核心功能 | 典型设备/实现 |
---|---|---|---|
应用层 | HTTP/FTP/SMTP | 应用程序间通信规范 | 浏览器、邮件客户端 |
传输层 | TCP/UDP | 端到端可靠传输、流量控制、拥塞控制 | Socket API |
网络层 | IP/ICMP | 逻辑寻址、路由选择、分组转发 | 路由器 |
网络接口层 | Ethernet/Wi-Fi | 物理传输、MAC地址管理 | 网卡/交换机 |
2. Socket的跨层特性
socket横跨传输层和应用层两个层级
- 向上,为应用层提供文件描述符的接口
- 向下,对接TCP/UDP的协议栈
socket编程
一、主机字节序和网络字节序
- 我们都知道字节序分为大端字节序和小端字节序,而如果我们在两台使用不同字节序的主机之间进行直接通信,那么接收端必定将其错误解释。
- 解决方法:发送端将数据转化为大端字节序然后发送,然后接收端根据自身采用的字节序来判断是否需要对数据进行转换,因此大端字节序也被称为网络字节序号。
- linux提供的四个字节序转换函数,函数最后的s和l分别代表short和long:
函数名 | 功能描述 | 示例 |
---|---|---|
htons() | 16位主机字节序转网络字节序 | 8888 → 0x22B8 |
ntohs() | 16位网络字节序转主机字节序 | 0x22B8 → 8888 |
htonl() | 32位主机字节序转网络字节序 | 8888 → 0x22B8 |
ntohl() | 32位网络字节序转主机字节序 | 0x22B8 → 8888 |
- 另外还有两个ip地址转换函数
函数名 | 功能描述 | 示例 |
---|---|---|
inet_addr() | 点分十进制转32位网络字节序 | “127.0.0.1” → 0x7f000001 |
inet_ntoa() | 32位网络字节序转点分十进制 | 0x7f000001 → “127.0.0.1” |
二、核心流程对比
服务器端步骤 | 客户端步骤 |
---|---|
1. 创建socket | 1. 创建socket |
2. 绑定地址和端口 | 2. 设置服务器地址 |
3. 监听端口 | 3. 发起连接请求 |
4. 接受客户端连接 | |
5. 进行数据通信 | 4. 进行数据通信 |
三、关键函数与代码解析
1. 创建Socket
这一步的目的是创建socket的文件描述符,相当于创建文件,但是文件的路径等其他信息我们并没有指定。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- 参数解析:
AF_INET
: IPv4地址族(32位地址+16位端口),常用可选参数还有AF_INET6(IPv6协议族)SOCK_STREAM
: 面向连接的可靠传输(TCP),常用可选参数还有SOCK_DGRAM,这表示传输层使用UDP协议。0
: 自动选择协议(TCP/UDP),即根据前面两个参数自动确定。
- 返回值:成功返回≥3的整数(0/1/2被标准输入输出占用)
2. 地址结构初始化
创建完文件描述符后,我们需要初始化socket的地址结构,也就是表明文件的路径。因为通用结构体并不好用,比如获取ip地址和端口号就要进行繁琐的位操作,这里我们使用的socket地址结构是IPv4专用结构体sockaddr_in。
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr)); // 清空结构体
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // IP转换
serv_addr.sin_port = htons(8888); // 端口转换
- sockaddr_in结构体:
struct sockaddr_in {
sa_family_t sin_family; // 地址族(2字节)
in_port_t sin_port; // 端口号(2字节)
struct in_addr sin_addr; // IPv4地址(4字节)
};
struct in_addr {
uint32_t s_addr; // 网络字节序的32位地址
};
- 字节序转换:
htons()
: Host to Network Short(16位端口)inet_addr()
: 点分十进制转32位网络字节序
- 参数解析
AF_INET
:指定结构体的地址族,因为我们使用的是IPv4专用socket地址,故而这里只能填AF_INETinet_addr("127.0.0.1")
:将点分十进制转化为网络字节序。赋值ip地址,否则通信另一方无法发现主机。htons(8888)
:将主机字节序转换为网络字节序。赋值端口,否则通信另一方无法找到通信程序。
3. 绑定地址
将文件描述符和socket地址进行绑定。
bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
- 参数解析:
sockfd
:需要绑定的文件描述符。(sockaddr*)&serv_addr
:sockaddr_in
(专用)转sockaddr
(通用),需要绑定的socket地址。sizeof(serv_addr)
:socket地址长度。
4. 监听端口
listen(sockfd, SOMAXCONN);
- 参数解析:
- sockfd:需要监听的文件描述符。
- SOMAXCONN:表示已完成连接队列的最大长度,一般直接用SOMAXCONN即系统定义的最大值
5. 接受连接
当listen监听到客户端想要connect的请求时,可以用accept函数接受这个连接,即获得客户端文件描述符。
struct sockaddr_in clnt_addr; //存储接收到的客户端的socket地址
socklen_t clnt_addr_len = sizeof(clnt_addr);// 计算客户端地址的长度,方便传入指针。
int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);//接受客户端文件描述符,以及将其和客户端socket地址绑定。
- 差异:需要注意的是accept函数和bind函数中的参数类型基本一致,只是第三个参数需要传递指针。
- socklen_t:
- 本质:
typedef unsigned int socklen_t
- 作用:表示socket地址结构的长度
- 特殊行为:
- 在accept()中作为值-结果参数(value-result argument)
- 输入时指定缓冲区大小,输出时返回实际地址长度
- 本质:
6. 客户端连接
运行在客户端上的函数,connect服务端上的listen函数,参数和bind函数并无不同
connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
三、代码示例
请注意,因为是示例代码,我并没有加入错误报告函数,但是这在socket编程中十分重要。
服务端代码 (server.cpp
):
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024
using namespace std;
int main() {
int serverSocket, clientSocket;
sockaddr_in serverAddr, clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
char buffer[BUF_SIZE];
// 创建socket
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
// 配置服务器地址
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
ser_addr.sin_port = htons(PORT);
// 绑定socket
bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
// 开始监听
listen(serverSocket, SOMAXCONN);
cout << "Server listening on port " << PORT << "..." << endl;
// 接受客户端连接
clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
cout << "Client connected!" << endl;
// 接收客户端数据
int bytesRead;
while ((bytesRead = recv(clientSocket, buffer, BUF_SIZE, 0)) > 0) {
buffer[bytesRead] = '\0'; // Null-terminate the received data
cout << "Received from client: " << buffer << endl;
// 发送回应
send(clientSocket, "Message received", 17, 0);
}
// 关闭socket
close(clientSocket);
close(serverSocket);
return 0;
}
客户端代码 (client.cpp
):
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024
using namespace std;
int main() {
int clientSocket;
sockaddr_in serverAddr;
char buffer[BUF_SIZE];
// 创建socket
clientSocket = socket(AF_INET, SOCK_STREAM, 0);
// 配置服务器地址
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
serverAddr.sin_port = htons(PORT);
// 连接到服务器
connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
cout << "Connected to server!" << endl;
// 发送数据到服务器
string message = "Hello, Server!";
send(clientSocket, message.c_str(), message.length(), 0);
// 接收服务器的回应
int bytesReceived = recv(clientSocket, buffer, BUF_SIZE, 0);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0'; // Null-terminate the received data
cout << "Received from server: " << buffer << endl;
}
// 关闭socket
close(clientSocket);
return 0;
}
编译与运行:
-
编译代码:
g++ server.cpp -o server g++ client.cpp -o client
-
运行服务端:
./server
-
运行客户端:
./client