需要使用的头文件:
cpp
#include <sys/socket.h> // 核心 socket 函数
#include <netinet/in.h> // IP 地址结构
#include <arpa/inet.h> // 字节序转换函数
#include <unistd.h> // close 函数 (Windows 下是 closesocket)
#include <string.h> // memset 等
这里用tcp举例
服务端流程
socket(): 创建一个监听套接字。bind(): 插上电话线(绑定 IP 和端口)。listen(): 开启接听模式。accept(): 等待客户打电话进来(阻塞),接通后产生一个新的套接字用于通信。recv()/send(): 收发数据。close(): 挂电话。
服务器端代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
int server_fd, client_fd;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
const char *hello = "Hello from server";
// 1. 创建 Socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 配置地址结构 (IPv4, 任意IP, 端口 8080)
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
address.sin_port = htons(8080); // 端口号,注意字节序转换
// 3. 绑定
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 4. 监听
if (listen(server_fd, 3) < 0) { // 队列长度为3
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server is listening on port 8080...\n");
// 5. 接受连接 (阻塞等待)
if ((client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 6. 读取数据
read(client_fd, buffer, 1024);
printf("Client says: %s\n", buffer);
// 7. 发送数据
send(client_fd, hello, strlen(hello), 0);
printf("Hello message sent\n");
// 8. 关闭
close(client_fd);
close(server_fd);
return 0;
}
客户端流程
socket(): 创建套接字。connect(): 拨打服务器电话(需要知道服务器 IP 和端口)。send()/recv(): 说话和听。close(): 挂电话。
客户端代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
char buffer[1024] = {0};
// 1. 创建 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(8080);
// 2. 将字符串IP转成网络格式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 3. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
// 4. 发送数据
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");
// 5. 接收回复
read(sock, buffer, 1024);
printf("Server reply: %s\n", buffer);
// 6. 关闭
close(sock);
return 0;
}
在Linux底下使用gcc编译:
cpp
# 编译 TCP 服务端
gcc server_tcp.c -o server_tcp
# 编译 TCP 客户端
gcc client_tcp.c -o client_tcp
# 运行 (先开服务端,再开客户端)
./server_tcp
./client_tcp
socket网络编程逻辑简单易懂,重点是明白API函数的作用,下一期会发表一篇有关三次握手四次挥手的文章重点讲解连接细节
核心API函数:
socket() ------ 创建套接字
该函数向操作系统申请创建一个网络通信的内核对象,返回一个文件描述符(File Descriptor)。你需要指定协议族(如 AF_INET 表示 IPv4)、套接字类型(如 SOCK_STREAM 表示流式 TCP 或 SOCK_DGRAM 表示数据报 UDP)以及具体的协议(通常设为 0,表示使用默认协议)。如果创建失败,返回 -1
bind() ------ 绑定地址
它将一个本地协议地址(sockaddr_in 结构体)赋予一个未命名的套接字。服务器端必须调用此函数来指定它监听的 IP 地址(如 INADDR_ANY 表示任意本地接口)和端口号(需转换为网络字节序)。对于客户端,通常可以省略此步骤,由内核自动分配临时端口
listen() ------ 监听连接
它通知内核,该套接字愿意接受连接请求。它会在内核中为该套接字维护两个队列:半连接队列 (SYN_RCVD 状态,三次握手未完成)和全连接队列 (ESTABLISHED 状态,握手已完成等待 accept 取走)。参数 backlog 指定了这两个队列长度之和的最大值
accept() ------ 接受连接
从监听套接字的已连接队列头部移除第一个连接请求,创建一个新的已连接套接字,并返回该套接字的描述符。注意: 原来的监听套接字(参数 sockfd)继续用于监听新的连接,而新返回的套接字专门用于与该特定客户端进行数据读写。如果队列为空,该函数默认会阻塞调用进程
connect() ------ 发起连接
该函数用于客户端主动向服务器发起连接请求。对于 TCP 套接字,它会触发三次握手过程;对于 UDP 套接字,它只是在内核中记录对端的 IP 和端口,使得后续的 send/recv 可以直接使用。调用成功表示连接已建立(TCP)或路由已确定(UDP)
send() / recv() ------ 数据传输
send(): 将应用层缓冲区的数据拷贝到内核发送缓冲区。对于 TCP,它返回实际写入内核缓冲区的字节数(可能小于请求长度,需循环发送);对于 TCP 连接,若返回 0 或错误,通常意味着对端关闭了连接。recv(): 从内核接收缓冲区读取数据到应用层缓冲区。它返回实际读取的字节数;若返回 0,表示对端已执行有序关闭(FIN 包);若返回 -1,表示出错。
注意事项:
htons() / htonl(): 主机字节序转网络字节序。由于不同机器(x86 小端 vs 网络大端)存储多字节数据的方式不同,端口号和 IP 地址在传输前必须使用这些函数转换,以保证协议一致性。
struct sockaddr_in: IPv4 套接字地址结构 。它是 sockaddr 的"友好版本",包含了协议族、端口和 IP 地址等字段。在调用 bind、connect 等函数时,通常需要将其强制转换为通用的 sockaddr* 类型。