目录
建立连接:listen()、accept()与TCP三次握手
一、C语言中<sys/socket.h>库的概览
<sys/socket.h>头文件包含的主要函数、数据类型及常量
<sys/socket.h>
是C语言网络编程中的核心头文件,它定义了创建、管理、关闭套接字所需的基本函数、数据类型和常量,为程序员提供了操作套接字的标准化接口。以下是该头文件中包含的主要元素:
1. 数据类型
-
socklen_t:一种无符号整型,用于表示与套接字相关的参数(如地址长度)的大小,确保跨平台兼容性。
-
sockaddr、sockaddr_in、sockaddr_in6等结构体:用于存储网络地址信息,如IP地址、端口号等。
sockaddr
是通用套接字地址结构,而sockaddr_in
和sockaddr_in6
分别用于IPv4和IPv6地址的具体表示。
2. 常量
-
AF_INET、AF_INET6:地址族常量,分别标识IPv4和IPv6协议。
-
SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等:套接字类型常量,分别对应TCP(面向连接的流套接字)、UDP(无连接的数据报套接字)和其他原始套接字类型。
-
SOL_SOCKET、IPPROTO_TCP、IPPROTO_UDP等:用于设置套接字选项时指定级别(level)的常量,如针对套接字本身的选项、TCP或UDP协议级别的选项。
-
其他与错误代码、标志位、选项值等相关的常量,如
EADDRINUSE
、SO_REUSEADDR
等。
3. 函数
-
socket():创建一个新的套接字,返回一个描述符,用于后续的套接字操作。需要指定地址族、套接字类型和协议。
-
bind():将一个套接字与本地地址(IP地址和端口号)绑定,使得该套接字可以接收发往该地址的连接请求或数据报。
-
listen():使一个TCP套接字进入被动监听状态,等待客户端的连接请求。需要指定同时可接纳的最大连接请求队列长度。
-
accept():在监听套接字上接受一个连接请求,返回一个新的已连接套接字描述符,用于与客户端进行数据交换。
-
connect():主动发起一个TCP连接,将一个套接字与远程地址(IP地址和端口号)建立连接。
-
send()、recv():在已连接的套接字上发送和接收数据。对于TCP,数据是按序、可靠的;对于UDP,数据是独立的数据报,可能丢失、重复或乱序。
-
sendto()、recvfrom():在数据报套接字上发送和接收数据,并可以指定或获取数据的来源/目标地址。
-
close():关闭一个套接字,释放与其相关的系统资源。
-
setsockopt()、getsockopt():设置和获取套接字的选项,如缓冲区大小、超时时间、是否复用地址等。
-
shutdown():关闭套接字的读、写或读写方向,用于控制连接的半关闭或完全关闭。
套接字(socket)的概念
套接字(socket)是网络通信中的端点,是进程间通信(IPC)的一种抽象。在操作系统层面,套接字是一种特殊的文件描述符,通过它,进程可以与其他进程(无论在同一台机器上还是在网络上的其他机器上)进行双向数据交换。
套接字具有以下特点:
-
跨进程通信:通过套接字,不同进程可以跨越进程边界,通过网络进行数据交互。
-
跨网络通信:套接字支持进程间通过网络进行通信,不受物理位置限制,只要网络可达即可。
-
双向通信:套接字支持双向数据传输,既可以发送数据,也可以接收数据。
-
协议无关性:虽然最常见的是基于TCP或UDP的套接字,但套接字本身是一个抽象概念,可以支持多种网络协议。
-
地址绑定:每个套接字都有一个唯一的地址(IP地址和端口号),用于标识网络中的特定通信端点。
总结
总的来说,<sys/socket.h>
头文件提供的函数、数据类型和常量构成了C语言网络编程的基础工具箱,使得程序员能够利用套接字这一抽象概念,以一致且标准化的方式在不同操作系统平台上实现复杂的网络通信功能。通过调用这些函数,开发者可以轻松创建、配置、操作和关闭套接字,实现基于TCP、UDP或其他协议的网络应用程序。
二、使用<sys/socket.h>实现TCP通信
创建套接字:socket()函数
参数含义、返回值解析与错误情况
socket()
函数是创建套接字的关键接口,其原型如下:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
参数:
domain
:指定通信域(地址族),如AF_INET
(IPv4)或AF_INET6
(IPv6)。type
:指定套接字类型,如SOCK_STREAM
(TCP)或SOCK_DGRAM
(UDP)。protocol
:指定使用的协议,一般设置为0,让系统自动选择与domain
和type
匹配的默认协议。
-
返回值:
- 若成功创建套接字,返回一个非负整数,即套接字描述符(socket descriptor),用于后续的套接字操作。
- 若失败,返回
-1
,并设置全局变量errno
为相应的错误代码,可通过perror()
或strerror()
获取错误信息。
可能出现的错误情况:
EINVAL
:参数非法,如指定的domain
、type
或protocol
组合不支持。EMFILE
:进程已打开的文件描述符数达到上限,无法再创建新的套接字。ENOBUFS
或ENOMEM
:系统资源不足,如内存不足,无法分配所需的资源来创建套接字。- 其他与系统资源、权限、网络状态等相关的错误。
地址结构与绑定:sockaddr_in与bind()
sockaddr_in数据结构
sockaddr_in
是用于IPv4地址的特定结构体,定义如下:
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; /* Address family (AF_INET) */
in_port_t sin_port; /* Port number in network byte order */
struct in_addr sin_addr; /* IPv4 address in network byte order */
char sin_zero[8]; /* Unused space for alignment */
};
sin_family
:设置为AF_INET
,表明这是一个IPv4地址结构。sin_port
:存放端口号,需转换为网络字节序(大端序)。sin_addr
:内部是一个in_addr
结构,存储IPv4地址,同样需要转换为网络字节序。sin_zero
:填充为零以确保结构体大小,通常不需要手动设置。
填充IP地址、端口号示例:
#include <arpa/inet.h>
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 将端口号转换为网络字节序
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr); // 填充IPv4地址
// 或者使用点分十进制字符串直接初始化
struct sockaddr_in client_addr = {
.sin_family = AF_INET,
.sin_port = htons(80),
.sin_addr = { .s_addr = inet_addr("8.8.8.8") } // 注意:inet_addr不检查输入有效性
};
bind()函数使用
bind()
函数用于将一个套接字与本地地址(IP地址和端口号)绑定:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
参数:
sockfd
:要绑定的套接字描述符。addr
:指向sockaddr
或其派生结构(如sockaddr_in
)的指针,包含要绑定的地址信息。addrlen
:地址结构的长度,如sizeof(struct sockaddr_in)
。
-
返回值:
- 成功返回0;失败返回
-1
,并设置errno
。
- 成功返回0;失败返回
建立连接:listen()、accept()与TCP三次握手
listen()函数
listen()
函数使一个已创建的TCP套接字进入被动监听状态,等待客户端的连接请求:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-
参数:
sockfd
:监听套接字描述符。backlog
:指定待处理连接请求的最大队列长度,超出时新来的连接请求可能被拒绝。
-
返回值:
- 成功返回0;失败返回
-1
,并设置errno
。
- 成功返回0;失败返回
accept()函数
accept()
函数用于从监听套接字中接受一个连接请求,返回一个新的已连接套接字描述符:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
-
参数:
sockfd
:监听套接字描述符。addr
:指向sockaddr
结构的指针,用于接收客户端的地址信息(可选,设为NULL时不接收)。addrlen
:指向地址结构长度的指针(入参为地址结构长度,出参为实际填充的长度)。
-
返回值:
- 成功返回已连接套接字描述符;失败返回
-1
,并设置errno
。
- 成功返回已连接套接字描述符;失败返回
connect()函数
connect()
函数在客户端用于主动发起一个TCP连接:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
参数:
sockfd
:已创建的客户端套接字描述符。addr
:指向包含服务器地址信息的sockaddr
结构的指针。addrlen
:地址结构的长度。
-
返回值:
- 成功返回0;失败返回
-1
,并设置errno
。
- 成功返回0;失败返回
TCP三次握手过程:
- 客户端:发送SYN(同步序列编号)段,请求与服务器建立连接。
- 服务器:收到SYN,回复SYN+ACK(同步确认)段,确认客户端的请求并同步序列号。
- 客户端:收到SYN+ACK,回复ACK(确认)段,确认服务器的同步信息。至此,TCP连接建立完成。
数据传输:send()与recv()
send()与recv()函数
send()
和recv()
函数分别用于在已连接的套接字上发送和接收数据:
#include <sys/socket.h>
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)。
-
返回值:
- 成功返回实际发送/接收的字节数;失败返回
-1
,并设置errno
。
- 成功返回实际发送/接收的字节数;失败返回
缓冲区管理和错误处理的重要性:
-
缓冲区:确保缓冲区足够大以容纳待发送/接收的数据,并在发送/接收后检查返回值以确定实际传输的字节数,避免缓冲区溢出或数据丢失。
-
错误处理:对
send()
和recv()
的返回值进行检查,遇到-1
时处理错误。常见的错误包括EAGAIN
(非阻塞模式下资源暂时不可用,需稍后重试)、EINTR
(系统调用被中断,可选择重试)等。
关闭连接:close()
close()函数
close()
函数用于关闭一个套接字,释放与之关联的系统资源:
#include <unistd.h>
int close(int fd);
-
参数:
fd
:要关闭的套接字描述符(或其他类型的文件描述符)。
-
返回值:
- 成功返回0;失败返回
-1
,并设置errno
。
- 成功返回0;失败返回
TCP通信结束时的正确关闭流程:
- 数据传输完毕:确保所有数据已发送并被对方确认接收。
- 关闭写端:调用
shutdown(sockfd, SHUT_WR)
,通知对方不再发送数据,进入半关闭状态。 - 接收剩余数据:继续调用
recv()
接收对方可能发送的剩余数据,直到收到EOF(返回0)或错误(如ECONNRESET
)。 - 关闭套接字:调用
close(sockfd)
彻底关闭套接字,释放资源。
遵循上述流程可以确保TCP连接的正常关闭,避免数据丢失或连接资源泄漏。在服务器端,通常在每个连接的生命周期内