网络面试题

socket 编程

服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),while-true里调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

server

以下是一个简单的使用C语言实现的Socket服务端代码示例,该服务端将持续监听特定端口,并接收来自客户端的连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// server
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

void error(int server_fd){
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}

int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {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(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
error(server_fd);
}

// 监听连接
if (listen(server_fd, 3) < 0) {
error(server_fd);
}

while(true){
printf("Waiting for a connection...\n");

// 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
error(server_fd);
}

// 读取客户端消息
read(new_socket, buffer, BUFFER_SIZE);
printf("Message received: %s\n", buffer);

char* response = "Nihao, Client!";
// 回显消息给客户端
send(new_socket, response, strlen(response)<=BUFFER_SIZE?strlen(response):BUFFER_SIZE, 0);
printf("Message echoed back to client\n");

close(new_socket); // 关闭与当前客户端的连接
}
// 关闭服务器套接字(一般不会到这一步,因为while(1)是无限循环)
close(server_fd);

return 0;
}
  1. 创建Socket:使用socket()函数创建一个套接字,AF_INET表示使用IPv4协议,SOCK_STREAM表示使用TCP协议。
  2. 绑定Socket:通过bind()函数将套接字绑定到特定的IP地址和端口号。
  3. 监听端口:使用listen()函数使套接字进入监听状态,等待客户端连接。
  4. 接受连接accept()函数会阻塞程序,直到有客户端连接。成功连接后会返回一个新的套接字用于与客户端通信。
  5. 读取数据:通过read()函数读取客户端发送的数据,并在服务器端打印出来。
  6. 发送响应:使用send()函数将响应消息发送回客户端。
  7. 关闭连接:通过close()函数关闭与客户端的连接。

这个服务端程序会持续运行并处理来自多个客户端的连接。

client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// client
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *message = "Hello, Server!";
char buffer[BUFFER_SIZE] = {0};

// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("Socket creation error\n");
return -1;
}

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将服务器地址转换为二进制
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("Invalid address/ Address not supported\n");
return -1;
}

// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("Connection Failed\n");
return -1;
}

// 发送消息到服务器
send(sock, message, strlen(message), 0);
printf("Message sent: %s\n", message);

// 读取服务器回显的消息
read(sock, buffer, BUFFER_SIZE);
printf("Message received from server: %s\n", buffer);

close(sock);
return 0;
}
  1. 创建Socket:使用socket()函数创建一个套接字,AF_INET表示使用IPv4协议,SOCK_STREAM表示使用TCP协议。
  2. 设置服务器地址:指定服务器的IP地址和端口号,inet_pton()函数将IPv4地址从文本形式转换为二进制形式。
  3. 连接服务器:通过connect()函数尝试与服务器建立连接。
  4. 发送消息:使用send()函数将消息发送给服务器。
  5. 读取响应:通过read()函数读取服务器返回的数据,并打印出来。
  6. 关闭套接字:通过close()函数关闭与服务器的连接。

运行这个客户端程序时,它会连接到在本地IP地址127.0.0.1、端口8080上运行的服务器,发送一个消息,然后接收并打印来自服务器的响应。

socket 函数

在上述C语言Socket编程示例中,涉及了几个关键的Socket相关函数和参数。以下是这些函数和参数的详细解释:

1. socket()

1
int socket(int domain, int type, int protocol);
  • 功能: 创建一个新的套接字(Socket),并返回一个文件描述符,用于后续的网络通信。
  • 参数:
    • domain: 协议族。常用的选项包括:
      • AF_INET: IPv4协议。
      • AF_INET6: IPv6协议。
    • type: 套接字类型。常用的选项包括:
      • SOCK_STREAM: 面向连接的TCP协议。
      • SOCK_DGRAM: 无连接的UDP协议。
    • protocol: 通常为0,表示使用默认协议。对于SOCK_STREAM,默认为TCP;对于SOCK_DGRAM,默认为UDP。

2. bind()

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能: 将套接字绑定到一个指定的IP地址和端口号上。
  • 参数:
    • sockfd: socket()返回的文件描述符。
    • addr: 一个指向struct sockaddr类型的指针,包含IP地址和端口号信息。
    • addrlen: addr结构体的大小(字节数)。

3. listen()

1
int listen(int sockfd, int backlog);
  • 功能: 使服务器套接字进入被动监听状态,等待客户端连接。
  • 参数:
    • sockfd: socket()返回的文件描述符。
    • backlog: 等待连接队列的最大长度。

4. accept()

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 功能: 从已连接队列中提取第一个连接请求,创建一个新套接字用于与客户端通信。
  • 参数:
    • sockfd: socket()返回的文件描述符(监听套接字)。
    • addr: 一个指向struct sockaddr类型的指针,用于存储客户端的IP地址和端口号。
    • addrlen: addr结构体的大小(字节数),传入时需要初始化,返回时会被更新为实际大小。

5. connect()

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能: 客户端使用此函数连接到指定的服务器地址和端口。
  • 参数:
    • sockfd: socket()返回的文件描述符。
    • addr: 一个指向struct sockaddr类型的指针,包含服务器的IP地址和端口号信息。
    • addrlen: addr结构体的大小(字节数)。

6. send()

1
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 功能: 通过套接字发送数据。
  • 参数:
    • sockfd: 套接字文件描述符。
    • buf: 指向要发送的数据的指针。
    • len: 要发送的数据的长度。
    • flags: 通常设为0。可以设置为其他标志位,如MSG_DONTWAIT(非阻塞发送)等。

7. recv()

1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 功能: 通过套接字接收数据。
  • 参数:
    • sockfd: 套接字文件描述符。
    • buf: 指向接收缓冲区的指针。
    • len: 接收缓冲区的大小。
    • flags: 通常设为0。可以设置为其他标志位,如MSG_DONTWAIT(非阻塞接收)等。

8. close()

1
int close(int fd);
  • 功能: 关闭套接字并释放相关资源。
  • 参数:
    • fd: 套接字文件描述符。

9. inet_pton()

1
int inet_pton(int af, const char *src, void *dst);
  • 功能: 将IP地址从文本格式转换为二进制格式,存储在struct sockaddr_instruct sockaddr_in6中。
  • 参数:
    • af: 地址族(例如,AF_INET表示IPv4)。
    • src: 以字符串形式表示的IP地址(例如 "127.0.0.1")。
    • dst: 用于存储转换后的二进制IP地址的缓冲区(通常为struct in_addrstruct in6_addr的成员)。

10. htons()

1
uint16_t htons(uint16_t hostshort);
  • 功能: 将主机字节序转换为网络字节序。常用于将端口号从主机字节序转换为网络字节序,以确保在网络传输中的正确性。
  • 参数:
    • hostshort: 要转换的端口号。

结构体 struct sockaddr_in

1
2
3
4
5
6
struct sockaddr_in {
short int sin_family; // 地址族(AF_INET)
unsigned short int sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[8]; // 填充字段,保持与`struct sockaddr`大小一致
};
  • 解释:
    • sin_family: 协议族,通常为AF_INET表示IPv4。
    • sin_port: 端口号,使用htons()将端口号转换为网络字节序。
    • sin_addr: struct in_addr结构体,表示IPv4地址。
    • sin_zero: 填充字段,用于保证struct sockaddr_instruct sockaddr大小一致。