此文章为本人学习笔记,若有错误求指正,自学推荐书籍《Linux/UNIX系统编程手册》,需要该书籍pdf文档可无偿分享。
一、Linux网络编程概述
Linux网络编程是指在Linux操作系统上开发网络应用程序的过程。网络编程的核心是Socket编程,Socket是操作系统提供的用于网络通信的接口。
1. 网络通信模型
1.1 OSI 七层模型
网络通信通常基于OSI模型,该模型分为七层:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。在实际编程中,我们主要关注传输层及以上的内容。
1.2 TCP/IP 五层参考模型
五层体系的协议结构是综合了 OSI 和 TCP/IP 优点的一种协议,包括应用层、传输层、网络层、数据链路层和物理层。其中应用层对应 OSI 的上三层,下四层和 OSI 相同。五层协议的体系结构只是为介绍网络原理而设计的,实际应用还是 TCP/IP 四层体系结构。
- 应用层 :为特定应用程序提供数据传输服务。
- 传输层 :为进程提供通用数据传输服务。
- 网络层 :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。
- 数据链路层 :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。
- 物理层 :负责比特流在传输介质上的传播。
2. TCP/IP协议
TCP/IP协议是互联网的核心协议,其中传输层协议TCP和UDP是网络编程中最常用的协议。
2.1 TCP
提供可靠的、面向连接的服务。常用于文件传输、邮件传输等对数据完整性要求高的场景。
TCP建立连接的三次握手:
TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- Client首先向Server发送连接请求报文段,同步自己的seq(x),Client进入SYN_SENT状态。
- Server收到Client的连接请求报文段,返回给Client自己的seq(y)以及ack(x+1),Server进入SYN_REVD状态。
- Client收到Server的返回确认,再次向服务器发送确认报文段ack(y+1),这个报文段已经可以携带数据了。Client进入ESTABLISHED状态。
- Server再次收到Client的确认信息后,进入ESTABLISHED状态。
TCP断开连接的四次握手:
- Client向Server发送断开连接请求的报文段,seq=m(m为Client最后一次向Server发送报文段的最后一个字节序号加1),Client进入FIN-WAIT-1状态。
- Server收到断开报文段后,向Client发送确认报文段,seq=n(n为Server最后一次向Client发送报文段的最后一个字节序号加1),ack=m+1,Server进入CLOSE-WAIT状态。此时这个TCP连接处于半开半闭状态,Server发送数据的话,Client仍然可以接收到。
- Server向Client发送断开确认报文段,seq=u(u为半开半闭状态下Server最后一次向Client发送报文段的最后一个字节序号加1),ack=m+1,Server进入LAST-ACK状态。
- Client收到Server的断开确认报文段后,向Server发送确认断开报文,seq=m+1,ack=u+1,Client进入TIME-WAIT状态。
- Server收到Client的确认断开报文,进入CLOSED状态,断开了TCP连接。
- Client在TIME-WAIT状态等待一段时间(时间为2*MSL((Maximum Segment Life)),确认Client向Server发送的最后一次断开确认到达(如果没有到达,Server会重发步骤(3)中的断开确认报文段给Client,告诉Client你的最后一次确认断开没有收到)。如果Client在TIME-WAIT过程中没有再次收到Server的报文段,就进入CLOSES状态。TCP连接至此断开
TCP:服务端和客户端的通信流程:
2.2 UDP
提供无连接的、不可靠的数据报服务。适用于对速度要求高,但对数据完整性要求不高的场景,如视频流、语音通信等。
二、Socket编程基础
Socket是网络编程的核心接口,用于建立和管理网络连接。
1.socket 编程流程
- 服务器
- 创建套接字(socket)
- 将socket与IP地址和端口绑定(bind)
- 监听被绑定的端口(listen)
- 接收连接请求(accept)
- 从socket中读取客户端发送来的信息(read)
- 向socket中写入信息(write)
- 关闭socket(close)
- 客户端
- 创建套接字(socket)
- 连接指定计算机的端口(connect)
- 向socket中写入信息(write)
- 从socket中读取服务端发送过来的消息(read)
- 关闭socket(close)
2. Socket相关函数
2.1 socket()函数 - 创建套接字
函数概述
socket()函数用于创建一个新的套接字,并返回套接字的文件描述符。如果套接字创建失败,函数将返回-1。
函数原型
int socket(int domain, int type, int protocol);
- domain: 指定协议族,如AF_INET(IPv4)或AF_INET6(IPv6)。
- type: 指定套接字类型,如SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。
- protocol: 通常为0,表示自动选择合适的协议。
示例代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
2.2 bind()函数 - 绑定套接字
函数概述
bind()函数将套接字与特定的IP地址和端口绑定,使其能够接收发送到该地址和端口的数据。
函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd: 套接字的文件描述符。
- addr: 指向sockaddr结构的指针,包含要绑定的地址和端口。
- addrlen: addr结构的大小。
示例代码
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
if (bind(sockfd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
2.3 listen()函数 - 监听被绑定的端口
函数概述
listen()函数将套接字设置为监听模式,准备接收来自客户端的连接请求。
函数原型
int listen(int sockfd, int backlog);
- sockfd: 套接字的文件描述符。
- backlog: 内核中连接队列的最大长度。
示例代码
if (listen(sockfd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
2.4 accept()函数 - 接收连接请求
函数概述
accept()函数从监听套接字的连接队列中取出第一个连接请求,生成一个新的套接字用于与客户端通信。
函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd: 监听套接字的文件描述符。
- addr: 客户端的地址结构指针,用于存储客户端信息。
- addrlen: addr结构的大小。
示例代码
int new_socket;
struct sockaddr_in client_address;
socklen_t addrlen = sizeof(client_address);
new_socket = accept(sockfd, (struct sockaddr *)&client_address, &addrlen);
if (new_socket < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
2.5 connect()函数 - 发送连接请求
函数概述
connect()函数用于客户端与服务器建立连接,向指定的服务器地址和端口发送连接请求。
函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd: 套接字的文件描述符。
- addr: 指向服务器sockaddr结构的指针。
- addrlen: addr结构的大小。
5.3 示例代码
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/Address not supported");
exit(EXIT_FAILURE);
}
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection Failed");
exit(EXIT_FAILURE);
}
2.6 send() 和 recv() 函数 - TCP发送、接收信息
函数概述
send():将数据发送到已连接的套接字。
recv():从已连接的套接字接收数据。
函数原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd: 套接字的文件描述符。
- buf: 指向发送或接收数据的缓冲区。
- len: 数据长度。
- flags: 传输标志,通常为0。
6.3 示例代码
char *message = "Hello, Server";
send(sockfd, message, strlen(message), 0);
char buffer[1024] = {0};
recv(sockfd, buffer, sizeof(buffer), 0);
printf("Message from server: %s\n", buffer);
2.7 sendto() 和 recvfrom() 函数 - UDP发送、接收消息
函数概述
- sendto():将数据发送到指定的地址(适用于UDP)。
- recvfrom():从指定的地址接收数据(适用于UDP)。
函数原型
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- dest_addr: 目标地址。
- src_addr: 源地址。
示例代码
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
char *message = "Hello, UDP Server";
sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
char buffer[1024] = {0};
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &addr_len);
printf("Message from server: %s\n", buffer);
三. TCP Socket编程
以下是一个简单的TCP服务器和客户端的实现。
TCP服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
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_fd, 3) < 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept failed");
exit(EXIT_FAILURE);
}
read(new_socket, buffer, 1024);
printf("Message: %s\n", buffer);
close(new_socket);
close(server_fd);
return 0;
}
TCP客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
char buffer[1024] = {0};
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);
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(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");
read(sock, buffer, 1024);
printf("Message: %s\n", buffer);
close(sock);
return 0;
}