原始套接字(Raw Socket)
原始套接字(Raw Socket)是一种提供较低级别网络访问的套接字。通过使用原始套接字,应用程序可以直接发送或接收网络层如IP的数据包,或者传输层如TCP、UDP的段,而无需通过常规的套接字API提供的协议处理。
以下是原始套接字的一些关键点:
-
协议独立性:使用原始套接字,我们可以操作或构建自己的协议,或者直接与现有协议(如ICMP)交互。
-
绕过内核处理:通常,当发送或接收数据包时,操作系统内核会为我们处理很多细节,例如TCP的三次握手或IP头的填充。但是,使用原始套接字,我们可以直接构建或解析这些协议,从而绕过标准的内核处理。
-
特权:由于原始套接字提供了对网络的低级访问,使用它们通常需要特权,例如root权限。
-
应用场景:
- 网络诊断和测试工具:例如,
ping
使用原始套接字发送和接收ICMP回显请求和回显响应。 - 定制协议的实现:例如,如果我们想实验一个新的传输层协议。
- 安全研究和网络攻击:例如,执行某些类型的DoS攻击或网络扫描。
- 网络诊断和测试工具:例如,
-
创建原始套接字:在Linux中,我们可以使用
socket
函数并为其提供AF_INET
(对于IPv4)或AF_INET6
(对于IPv6)以及SOCK_RAW
来创建一个原始套接字。 -
手动头部处理:使用原始套接字,需要手动构建或解析协议头部。例如,如果我们正在发送一个TCP段,我们需要手动构建IP和TCP头部,并设置所有必要的字段。同样,当从一个原始套接字接收数据时,我们将获取整个数据包,需要自己解析它。
-
混杂模式:如果我们想使用原始套接字捕获一个接口上的所有流量(而不仅仅是发给特定地址的流量),我们需要将接口设置为混杂模式。
需要注意的是,虽然原始套接字提供了强大的功能,但也需要小心使用。手动处理协议细节容易导致错误,并可能引起网络问题或安全隐患。
Socket()
socket()
函数是计算机网络编程中的核心函数之一,用于创建一个新的套接字。套接字是端到端的通信链路,是进程之间进行网络通信的主要手段。
下面详细介绍socket()
函数:
函数原型
int socket(int domain, int type, int protocol);
参数
-
domain(或称为family):指定使用哪种地址族。常见的选择包括:
AF_INET
:IPv4 地址族。用于IPv4网络通信。AF_INET6
:IPv6 地址族。用于IPv6网络通信。AF_UNIX
:本地套接字(UNIX 域套接字)地址族。用于同一机器上的进程间通信。
-
type:指定套接字的类型。常见的选择包括:
SOCK_STREAM
:提供面向连接、可靠、双向的字节流服务。典型的协议有TCP。SOCK_DGRAM
:提供无连接的、不可靠的数据报服务。典型的协议有UDP。SOCK_RAW
:提供原始套接字访问,允许直接发送或接收协议如IP的数据包。SOCK_SEQPACKET
:提供面向连接的、可靠的、固定最大长度的记录序列。
-
protocol:指定要使用的协议。通常,当给定了套接字的类型时,可以将此参数设置为0,让系统自动选择合适的协议。例如,当
type
是SOCK_STREAM
时,系统通常选择TCP作为协议。
返回值
- 成功:返回一个非负描述符,代表新创建的套接字。
- 失败:返回-1,并设置相应的错误码。
使用示例
创建一个用于IPv4 TCP通信的套接字:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
创建一个用于IPv4 UDP通信的套接字:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
注意事项
- 创建套接字只是第一步。为了实际上进行通信,还需要其他函数,如
bind()
,listen()
,accept()
,connect()
,send()
, 和recv()
,来配置并操作这个套接字。 - 对于TCP服务端,通常在
socket()
之后会调用bind()
,listen()
和accept()
来绑定地址、监听连接和接受连接。 - 对于TCP客户端,通常在
socket()
之后会调用connect()
来连接到服务器。 - 对于UDP,没有建立或接受连接的概念,所以只需创建套接字,然后可以直接使用
sendto()
和recvfrom()
进行通信。
socket()
函数是网络编程中的基础,几乎所有的网络应用程序都会在某个地方使用它来开始其网络通信。
bind()
bind()
是套接字编程中的一个关键函数,用于将套接字与特定的IP地址和端口号绑定。它通常在服务器设置期间使用,以指定服务器将在哪个地址和端口监听即将到来的客户端连接。
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
-
sockfd: 这是一个套接字描述符,它表示要绑定的套接字。
-
addr: 这是一个指向
struct sockaddr
的指针,该结构定义了套接字的地址(IP和端口)。在实践中,通常使用特定于协议的结构(如struct sockaddr_in
对于IPv4)来填充这个参数,并将其指针类型强制转换为struct sockaddr *
。 -
addrlen: 这是地址结构的大小(例如,对于IPv4,这将是
sizeof(struct sockaddr_in)
)。
返回值:
- 成功时,
bind()
返回0。 - 失败时,返回-1,并设置
errno
以指示错误的原因。
常见的使用模式:
在服务器中,通常首先创建一个套接字,然后使用bind()
将它绑定到一个地址和端口。以下是一个简化的示例,演示如何使用bind()
为IPv4地址绑定套接字:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // Create a TCP socket
if (sockfd < 0) {
perror("Error creating socket");
return 1;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // Address family for IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // Listen on any interface
server_addr.sin_port = htons(8080); // Listen on port 8080
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Error binding socket");
return 1;
}
// ... the server can then proceed to listen and accept connections
return 0;
}
常见错误和注意事项:
-
Address already in use: 如果尝试绑定到已被另一个套接字使用的地址和端口,将会出现此错误。这通常发生在服务器崩溃并尝试重新启动,但由于之前的套接字仍处于“TIME_WAIT”状态,所以它不能立即绑定。使用
setsockopt()
和SO_REUSEADDR
可以帮助解决此问题。 -
Permission denied: 通常,只有root用户才能绑定到低于1024的端口。
-
确保在绑定套接字之前填充了整个
sockaddr_in
结构,并正确设置了sin_family
、sin_addr.s_addr
和sin_port
字段。 -
使用
htons()
函数确保端口号是网络字节顺序。
通过合理地使用bind()
函数,开发人员可以确保他们的服务器监听特定的IP地址和端口,从而等待客户端的连接。
listen()
listen()
是套接字API中的一个函数,用于让一个套接字进入监听模式,从而能够接收来自客户端的连接请求。这是创建服务器应用程序的必要步骤之一。
函数原型:
int listen(int sockfd, int backlog);
参数:
-
sockfd: 一个套接字描述符,它应该先前已经使用
socket()
创建并使用bind()
绑定到一个特定的地址和端口。 -
backlog: 这个参数定义了等待队列的大小,也就是说,系统应该允许等待处理(未
accept()
的)的连接数量。当有更多的客户端尝试连接,超过了backlog指定的数量时,系统会开始拒绝这些新的连接请求。
返回值:
- 如果函数调用成功,则返回0。
- 如果出现错误,则返回-1,并设置
errno
以指示出现的特定错误。
使用:
一旦使用bind()
函数将套接字绑定到一个地址和端口后,我们可以调用listen()
以进入监听模式。在此模式下,套接字准备接受来自客户端的连接请求。
这是一个简单的示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // Create a socket
// ... (bind the socket to an address using bind() here)
if (listen(sockfd, 5) < 0) { // Allow up to 5 pending connections
perror("Error while trying to listen");
return 1;
}
此示例创建了一个套接字,并设置其最大待处理连接数为5。当超过5个客户端连接并等待被accept()
时,任何进一步的连接请求都将被拒绝,直到有一个连接被accept()
为止。
注意:
- 在调用
listen()
之前,必须先调用bind()
。 backlog
参数的具体含义和行为可能因操作系统而异。在某些系统上,它表示待处理的连接数量,而在其他系统上,它可能包括已被accept()
但尚未由应用程序处理的连接。- 当
backlog
队列已满,进一步的连接请求可能会被拒绝。因此,为了避免这种情况,服务器应该尽快处理连接。 - 通常,在
listen()
之后,会进入一个循环,不断地调用accept()
来接受并处理来自客户端的连接。
总之,listen()
函数是服务器套接字编程中的关键步骤,使得服务器能够开始接受客户端的连接请求。
accept()
accept()
是套接字编程中的一个关键函数,用于从已经处于监听模式的套接字中提取连接请求,并返回一个新的套接字描述符,该描述符代表与客户端之间的新连接。此函数在服务器应用程序中经常使用,以处理来自客户端的连接请求。
函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
-
sockfd: 这是一个处于监听模式的套接字描述符,通常先前已经通过
socket()
创建并通过bind()
和listen()
函数配置。 -
addr: 这是一个指向
struct sockaddr
的指针,当accept()
调用返回时,这个结构将被填充与已经接受的连接的远程端(客户端)的地址信息。 -
addrlen: 这是一个值-结果参数。在调用
accept()
之前,它应该被设置为addr
指向的地址结构的大小。当函数返回时,addrlen
将被设置为实际的地址大小。
返回值:
-
成功时,
accept()
返回一个新的套接字描述符,代表与客户端的新连接。此新描述符应用于后续的所有通信(例如send()
和recv()
调用)。 -
失败时,返回 -1,并设置
errno
以指示出现的错误。
使用和注意事项:
-
在服务器应用程序中,通常在
listen()
函数调用后立即调用accept()
,等待客户端的连接。 -
accept()
函数是阻塞的,这意味着它将等待,直到一个连接请求可用,除非套接字已被配置为非阻塞。 -
返回的新套接字描述符与原始的监听套接字是独立的。应使用新的套接字描述符进行与客户端的所有通信,并继续使用原始的监听套接字来接受其他连接请求。
-
通常,服务器将为每个接受的连接启动一个新的线程或进程以并行处理多个连接。
-
addr
和addrlen
参数是可选的;如果我们不关心客户端的地址,我们可以设置这两个参数为 NULL。
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int server_sock, client_sock;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// Assuming server_sock has been created and set up for listening...
client_sock = accept(server_sock, (struct sockaddr *) &client_addr, &client_addr_len);
if (client_sock < 0) {
perror("Error on accept");
return 1;
}
// Use client_sock for communication with the client...
close(client_sock);
return 0;
}
这个简单的例子展示了如何使用 accept()
函数从 server_sock
监听套接字中接受一个新的连接,并使用 client_sock
与客户端进行通信。
connect()
connect()
是套接字编程中的一个函数,主要用于客户端应用程序。该函数使客户端尝试与服务器端的指定地址建立连接。
函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
-
sockfd: 这是我们想要与远程主机连接的套接字的描述符。
-
addr: 这是一个指向
struct sockaddr
的指针,包含我们想要连接的远程主机的地址信息。在实际应用中,通常使用特定于协议的结构(如struct sockaddr_in
对于IPv4)并将其类型强制转换为struct sockaddr *
。 -
addrlen: 这是地址结构的大小,例如对于IPv4地址,这通常是
sizeof(struct sockaddr_in)
。
返回值:
-
成功时,
connect()
返回0。 -
失败时,返回-1,并设置
errno
以指示错误原因。
使用和注意事项:
-
在客户端应用程序中,我们通常首先使用
socket()
函数创建一个套接字,然后使用connect()
函数尝试与服务器连接。 -
如果
connect()
成功,客户端可以开始使用send()
和recv()
或其他相关函数与服务器通信。 -
如果连接尝试失败,
connect()
将返回-1。这可能是由于多种原因,例如服务器未在指定的地址和端口上运行,网络故障,或服务器拒绝连接。 -
connect()
在默认情况下是阻塞的,这意味着它会等待,直到连接成功或发生错误。但是,我们可以将套接字设置为非阻塞模式,使connect()
立即返回,并后续使用select()
或poll()
来等待连接完成。
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int client_sock;
struct sockaddr_in server_addr;
// Assuming client_sock has been created...
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // server port
inet_pton(AF_INET, "192.168.1.1", &server_addr.sin_addr); // server IP
if (connect(client_sock, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) {
perror("Error on connect");
return 1;
}
// Now client_sock is connected and can be used to send or receive data...
return 0;
}
这个简单的例子展示了如何使用 connect()
函数尝试与运行在192.168.1.1
的服务器上的服务连接,该服务监听端口8080
。
send()
send()
是套接字编程中的一个函数,用于向一个已连接的套接字发送数据。它通常用于TCP套接字,但也可以与其他类型的套接字一起使用。
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
-
sockfd: 这是一个已连接的套接字的描述符。
-
buf: 这是一个指针,指向我们想要发送的数据的缓冲区。
-
len: 这是
buf
中我们想要发送的数据的字节数。 -
flags: 这是一个修改函数操作的标志集合。常见的标志包括
MSG_OOB
(用于发送"out-of-band"数据) 和MSG_NOSIGNAL
(阻止在连接断开时发送SIGPIPE信号)。大多数情况下,我们可以简单地将此参数设置为0。
返回值:
-
成功时,
send()
返回实际发送的字节数。请注意,这可能少于我们请求发送的数量。 -
失败时,返回-1,并设置
errno
以指示错误原因。
使用和注意事项:
-
在一个已连接的TCP套接字上使用
send()
之前,必须先成功地调用connect()
(对于客户端)或accept()
(对于服务器)。 -
TCP是一个流协议,这意味着没有消息边界。连续的
send()
调用可能会在接收方看起来像一个连续的数据流,而不是单独的消息。 -
如果套接字是阻塞的(默认情况),
send()
可能会阻塞,直到有足够的网络缓冲区可用以发送数据。如果套接字是非阻塞的,而网络缓冲区不可用,则send()
将立即返回-1,并将errno
设置为EAGAIN
或EWOULDBLOCK
。 -
在连接断开的套接字上调用
send()
将导致发送一个SIGPIPE
信号,除非设置了MSG_NOSIGNAL
标志。此信号的默认行为是终止进程,但可以捕获或忽略它。
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
int main() {
int sockfd; // assuming it's already connected
const char *message = "Hello, server!";
ssize_t bytes_sent;
bytes_sent = send(sockfd, message, strlen(message), 0);
if (bytes_sent < 0) {
perror("Error on send");
return 1;
}
printf("Sent %zd bytes to server.\n", bytes_sent);
return 0;
}
这个简单的示例展示了如何使用send()
函数将一条消息发送到一个已连接的服务器。
recv()
recv()
函数用于从已连接的套接字接收数据。它主要用于 TCP 套接字,但也可以与其他类型的套接字一起使用。
函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
-
sockfd: 这是一个已连接的套接字的描述符。
-
buf: 这是一个指针,指向一个缓冲区,用于存储接收到的数据。
-
len: 这是缓冲区的大小,即我们期望接收的最大字节数。
-
flags: 这是一个修改函数操作的标志集合。一些常见的标志包括:
MSG_PEEK
: 查看即将到来的数据,但不从队列中删除它。MSG_WAITALL
: 尝试接收指定的len
字节。与默认行为不同,该标志会使函数等待,直到请求的字节数量可用或发生某些错误。MSG_OOB
: 用于接收 “out-of-band” 数据。
在大多数常规操作中,我们可以简单地将此参数设置为0。
返回值:
-
成功时,
recv()
返回实际接收到的字节数。如果连接已关闭,返回0。 -
失败时,返回-1,并设置
errno
以指示错误原因。
使用和注意事项:
-
在一个已连接的 TCP 套接字上使用
recv()
之前,我们需要先成功调用connect()
(对于客户端)或accept()
(对于服务器)。 -
TCP 是一个流协议,这意味着没有消息边界。连续的
recv()
调用可能会接收到之前调用的数据的剩余部分。 -
如果套接字是阻塞的(默认情况),并且没有数据可用,
recv()
会阻塞,直到数据可用。如果套接字是非阻塞的,并且没有数据可用,recv()
会立即返回-1,并将errno
设置为EAGAIN
或EWOULDBLOCK
。 -
当连接断开或关闭时,
recv()
将返回0。因此,接收到0字节通常意味着对端关闭了连接。
示例:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
int main() {
int sockfd; // assuming it's already connected
char buffer[1024];
ssize_t bytes_received;
bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received < 0) {
perror("Error on recv");
return 1;
} else if (bytes_received == 0) {
printf("The peer has closed the connection.\n");
return 0;
}
buffer[bytes_received] = '\0'; // Null-terminate the string
printf("Received: %s\n", buffer);
return 0;
}
这个简单的示例展示了如何使用 recv()
函数从已连接的服务器接收消息,并将其打印出来。
综合案例
下面是一个简单的TCP套接字编程的例子,其中包括一个服务器和一个客户端。服务器接收来自客户端的消息,然后返回相同的消息。
服务器端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_sock, client_sock;
struct sockaddr_in server_addr, client_addr;
char buffer[BUFFER_SIZE];
int bytes_read;
// 创建套接字
server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
perror("Socket creation failed");
exit(1);
}
// 设置服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
exit(1);
}
// 监听连接请求
listen(server_sock, 5);
printf("Server is listening on port %d...\n", SERVER_PORT);
socklen_t client_addr_len = sizeof(client_addr);
client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_sock == -1) {
perror("Accept failed");
exit(1);
}
// 读取和响应客户端的消息
bytes_read = recv(client_sock, buffer, BUFFER_SIZE, 0);
buffer[bytes_read] = '\0';
printf("Received from client: %s\n", buffer);
send(client_sock, buffer, bytes_read, 0);
close(client_sock);
close(server_sock);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
int client_sock;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建套接字
client_sock = socket(AF_INET, SOCK_STREAM, 0);
if (client_sock == -1) {
perror("Socket creation failed");
exit(1);
}
// 设置服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
// 连接到服务器
if (connect(client_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Connect failed");
exit(1);
}
strcpy(buffer, "Hello, Server!");
send(client_sock, buffer, strlen(buffer), 0);
int bytes_received = recv(client_sock, buffer, BUFFER_SIZE, 0);
buffer[bytes_received] = '\0';
printf("Received from server: %s\n", buffer);
close(client_sock);
return 0;
}
运行结果如下:
上述示例中,服务器创建一个套接字,绑定到本地地址并监听连接。当客户端连接时,服务器接收来自客户端的消息,并将相同的消息发送回客户端。客户端则发送一个简单的消息,并从服务器接收响应。