概述
在网络编程中,套接字(Socket)是一种用于进程间通信的接口。套接字是操作系统提供的一种抽象层,它允许不同计算机之间的进程通过网络进行通信。套接字实际上并不神秘,简单来说,套接字是连接网络中不同主机上应用程序的桥梁,通过套接字,应用程序可以发送和接收数据。
套接字有多种类型,最常见的两种是:流式套接字和数据报套接字。
流式套接字:基于TCP协议,提供面向连接的、可靠的数据传输服务。数据在传输过程中会被组织成无边界的字节流,并按照发送顺序到达接收端。
数据报套接字:基于UDP协议,提供无连接的、不可靠的数据传输服务。每个数据报都是独立的,系统不保证数据报的顺序,也不保证一定到达。
基本步骤
无论是客户端还是服务器,使用套接字进行网络编程通常包括以下几个主要步骤。
1、创建套接字。创建一个新的套接字,用于网络通信。创建时,需要指定地址族(IPv4、IPv6等)、套接字类型(TCP、UDP等)。
2、绑定地址。仅对服务器有效,将套接字绑定到一个特定的IP地址和端口,以便客户端可以连接到它。
3、监听连接。可选,仅对TCP服务器有效,服务器开始监听指定端口上的连接请求。
4、接受连接。仅对TCP服务器有效,服务器接受来自客户端的连接请求。这将返回一个新的套接字,用于与该客户端的网络通信。
5、主动连接。仅对TCP客户端有效,客户端主动连接到服务器。主动连接时,需要指定服务器的IP地址和端口。
6、发送/接收数据。客户端与服务器通过套接字发送和接收数据,数据的内容和格式由应用层指定。
7、关闭套接字。完成网络通信后,关闭套接字,释放相应的资源。
为了更加形象地理解TCP网络通信中客户端和服务器的具体行为,可以参考下面的时序图。
接口介绍
C++并没有内置的套接字编程库,但可以使用C语言中的套接字API来实现网络通信。这些API通常是POSIX标准的一部分,在<sys/socket.h>头文件中定义。常用的套接字编程接口如下。
1、socket函数:用于创建一个新的套接字。
int socket(int domain, int type, int protocol);
domain:指定协议族,取值一般为AF_INET(对于IPv4)、AF_INET6(对于IPv6)。
type:指定套接字类型,取值一般为SOCK_STREAM(对于TCP)、SOCK_DGRAM(对于UDP)。
protocol:通常设置为0,表示选择默认协议。
返回值:成功时返回一个套接字描述符,失败时返回-1。
2、bind函数:用于将一个套接字绑定到一个特定的地址和端口。这个函数是网络编程中的一个重要步骤,特别是在服务器编程中,因为它允许服务器监听特定的IP地址和端口。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addr:指向sockaddr结构体的指针,包含地址信息。sockaddr是一个通用的、最小化的结构体,其定义如下。其中,sa_family是一个整数,表示地址族,常见的值为AF_INET(对于IPv4)、AF_INET6(对于IPv6)。对于IPv4地址,通常使用sockaddr_in结构体。对于IPv6地址,通常使用sockaddr_in6结构体。
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
struct sockaddr_in
{
sa_family_t sin_family; // 地址族,通常是AF_INET
in_port_t sin_port; // 端口号,网络字节序
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr
{
uint32_t s_addr; // IPv4地址,网络字节序
};
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址族,通常是AF_INET6
in_port_t sin6_port; // 端口号,网络字节序
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr
{
unsigned char s6_addr[16]; // 16字节的IPv6地址
};
addrlen:addr的长度,通常是sizeof(sockaddr_in) 或 sizeof(sockaddr_in6)。
返回值:成功时返回0,失败时返回-1。
3、listen函数:用于将套接字设置为监听状态,准备接受连接请求。该函数仅对TCP服务器有效。
int listen(int sockfd, int backlog);
sockfd:套接字描述符。
backlog:未完成连接队列的最大长度,主要作用是控制服务器在处理大量并发连接时的行为。当服务器收到大量的连接请求时,内核会将这些请求放入队列中。如果队列已满,新的连接请求可能会被拒绝或丢弃。如果 backlog设置得太小,可能会导致一些连接请求被拒绝。如果backlog设置得太大,可能会占用更多的系统资源,但可以更好地处理突发的大量连接请求。
返回值:成功时返回0,失败时返回-1。
4、accep函数:用于从监听套接字上接受一个连接请求。当客户端尝试连接到服务器时,服务器会使用accept函数来接收这个连接,并创建一个新的套接字描述符,以便与该客户端进行通信。该函数仅对TCP服务器有效。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:监听套接字描述符。
addr:指向sockaddr结构体的指针,用于存储客户端的地址信息。
addrlen:指向addr长度的指针。
返回值:成功时返回新的连接套接字描述符,失败时返回-1。
5、connect函数:用于发起一个连接请求到指定的服务器,以便与服务器建立连接。该函数仅对TCP客户端有效。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addr:指向sockaddr结构体的指针,包含目标地址信息。
addrlen:addr 的长度。
返回值:成功时返回0,失败时返回-1。
6、send函数:用于向已连接的套接字发送数据,通常用于TCP套接字。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字描述符。
buf:指向要发送的数据缓冲区的指针。
len:要发送的数据长度。
flags:控制发送操作的标志,常用标志的取值如下。
(1)0:默认值,无特殊行为。
(2)MSG_OOB:发送带外数据。
(3)MSG_DONTROUTE:不要路由,直接发送到本地网络。
(4)MSG_NOSIGNAL:当对端关闭时,防止SIGPIPE信号。
返回值:成功时返回实际发送的字节数,失败时返回-1。
7、recv函数:用于从已连接的套接字接收数据,通常用于TCP套接字。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:套接字描述符。
buf:指向接收数据缓冲区的指针。
len:缓冲区的最大长度。
flags:控制接收操作的标志,常用标志的取值如下。
(1)0:默认值,无特殊行为。
(2)MSG_OOB:接收带外数据。
(3)MSG_PEEK:查看数据,而不从输入队列中移除。
(4)MSG_WAITALL:等待直到接收了指定数量的字节或连接关闭(可能被信号中断)。
返回值:成功时返回实际接收的字节数,失败时返回 -1。
8、sendto函数:用于向指定的目标地址发送数据,通常用于UDP套接字。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:套接字描述符。
buf:指向要发送的数据缓冲区的指针。
len:要发送的数据长度。
flags:控制发送操作的标志。
dest_addr:指向目标地址的sockaddr结构体。
addrlen:dest_addr的长度。
返回值:成功时返回实际发送的字节数,失败时返回-1。
9、recvfrom函数:用于从一个无连接的套接字接收数据,并且可以获取发送方的地址信息,通常用于UDP套接字。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:套接字描述符。
buf:指向接收数据缓冲区的指针。
len:缓冲区的最大长度。
flags:控制接收操作的标志。
src_addr:指向发送方地址的sockaddr结构体。
addrlen:src_addr的长度。
返回值:成功时返回实际接收的字节数,失败时返回-1。
10、close函数:用于关闭文件描述符,包括套接字。在调用close函数之前,确保所有未发送的数据都已经发送完毕。如果还有待发送的数据,这些数据可能会被丢弃。对于接收端,确保已经读取了所有来自对端的数据。一旦调用了close函数,就无法再从该套接字接收数据。好的编程习惯是:一旦不再需要套接字,就立即调用close函数,以释放相关的系统资源。
int close(int sockfd);
sockfd:套接字描述符。
返回值:成功时返回0,失败时返回-1。