Linux网络编程基础以及第一个TCP服务器
- 一、什么是 IO ?
- 二、网络编程接口函数
-
- 2.1、socket() 函数
- 2.2、bind() 函数
- 2.3、listen() 函数
- 2.4、accept() 函数
- 2.5、fcntl() 函数
- 2.6、connect() 函数
- 2.7、send() 函数
- 2.8、recv() 函数
- 2.9、close() 函数
- 三、阻塞和非阻塞的区别
- 四、如何开发一个TCP服务器?
- 五、总结
一、什么是 IO ?
IO 是 input/output的简写,即输入输出。在操作系统以上的软件层面,特别是在Linux上的 IO 都可以视为 fd (文件描述符),网络 IO 也可以视为 socket。
下图是TCP服务器与客户端的交互示意图:
TCP服务会监听一个端口,客户端连接到端口时服务器会为其生成对应的 listen fd(每一个TCP连接都会在服务器生成相应的socket)。
只要能构建出输入和输出,在口语表述的时候都可以称为IO。
网络 IO 编程主要围绕两个核心:
二、网络编程接口函数
假设我们要实现一个 TCP 服务器,我们就要先了解有哪些接口可以使用。在Linux中,使用C++进行网络编程通常涉及几个基本的socket函数。
那么什么是 socket 呢?
socket 直接翻译过来叫做 “插座”,很难与 IO 联想到一起。插座一般分为两个部分,即插头和座排;socket也同样如此,带有两层属性:
这两个是联系在一起的,当我们调用write或read这个函数的时候,它通过 fd 查找到对应的收发网络控制块,然后把数据放到里面去。
2.1、socket() 函数
函数原型:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain | 指定协议族(地址族),决定使用哪种网络协议。 | AF_INET (IPv4) AF_INET6 (IPv6) AF_UNIX (本地通信) AF_PACKET (链路层) |
type | 指定套接字类型,决定套接字的传输协议类型。 | SOCK_STREAM (TCP,面向连接) SOCK_DGRAM (UDP,无连接) SOCK_RAW (原始套接字) SOCK_SEQPACKET (面向消息的可靠连接) |
protocol | 指定具体的传输协议,通常设为0以选择默认协议。 | IPPROTO_TCP (用于 TCP) IPPROTO_UDP (用于 UDP) 0 (使用默认) |
返回值:成功时返回一个非负整数(socket描述符),失败时返回 -1。
socket()函数为什么一直是使用三个参数呢?
这是有一定历史原因在里面的,几十年了一直都这么写;这三个参数中,除了第二个参数可能要变之外,其他的参数可能都没有变过(当然,这说到是绝大多数情况下)。比如,第一个参数是指定协议族,要指定的原因是因为几十年前不仅仅只有TCP协议,还有其他各式各样的协议同时存在(百花齐放时期),那时候就是用第一个参数来指定的,只是后面经过淘汰,就只留下了AF_*几个(目前用的最多的还是AF_INET)。然而,随着历史的迁移,为了兼容性,这三个参数就一直没有更改过。
这里可能会有一个疑问,socket是一个int类型的值,为什么是int的呢? 这意味着后面很多东西是基于int值进行操作的,socket默认从0开始,依次递增,其中0,1,2是系统确定的,代着这标准输入输出,即stdin、stdout、stderr。
2.2、bind() 函数
socket()函数解决了fd的问题,即插口,还需要使用bind()函数提供“座排”,即网络部分。
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: socket描述符。
- addr: 指向 sockaddr 结构的指针,包含要绑定的地址信息。
- addrlen: 地址结构的大小。
返回值:成功时返回0,失败时返回-1。
要想使用bind()函数,还需要了解一个非常重要的结构体:struct sockaddr_in ,它是在 C 和 C++ 编程中用于表示 IPv4 地址的结构体,其定义通常在 <netinet/in.h> 头文件中。
struct sockaddr_in 结构体定义:
struct sockaddr_in {
short sin_family; // 地址族,通常为 AF_INET
unsigned short sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IPv4 地址
char sin_zero[8]; // 填充字段,用于保持结构体的对齐
};
sin_family | short | 地址族字段。对于 IPv4,通常设置为 AF_INET。 其他的还有AF_INET6 (IPv6)、 AF_UNIX (本地通信)、 AF_PACKET (链路层)。 |
sin_port | unsigned short | 16 位端口号,使用网络字节序(Big Endian)。可以使用 htons() 函数转换主机字节序到网络字节序。 |
sin_addr | struct in_addr | 表示 IPv4 地址的结构体,包含一个无符号长整型 s_addr。可以使用 inet_pton() 函数将字符串形式的地址转换为此格式。 |
sin_zero | char[8] | 保留字段,用于填充,确保 struct sockaddr 的大小与 sockaddr_in 结构体的一致性。 通常不使用,可将其置为0。 |
其中sin_addr 是一个类型为 struct in_addr 的结构体,其定义如下:
struct in_addr {
unsigned long s_addr; // 32 位 IPv4 地址,以网络字节序存储
};
struct in_addr 结构体用于表示 IPv4 地址,关键字段是 s_addr,它是一个无符号长整型(32 位)。设置 s_addr 的值可以用来指定不同的网络地址。常用的 struct in_addr 值:
INADDR_ANY | 0.0.0.0 | 表示接收来自任意 IP 地址的连接。服务器常用此地址进行绑定,允许接收来自所有接口的请求。 |
INADDR_LOOPBACK | 127.0.0.1 | 本地回环地址,表示本机,常用于测试和调试网络应用。通常可用于测试本地服务。 |
INADDR_BROADCAST | 255.255.255.255 | 广播地址,用于向网络中所有主机发送数据包。适用于需要向所有网络上的设备发送数据的情况。 |
INADDR_NONE | 0xffffffff | 用于指示无效地址,无论何时应视为无效或未指定。 |
INADDR_ALLHOSTS_GROUP | 224.0.0.1 | 特殊的多播地址,用于IPv4多播,表示所有主机组,接收此地址在同一子网的所有主机。 |
要想绑定到一个指定的 IPv4 地址(例如 192.168.1.1),需要使用 inet_pton() 函数来将字符串形式的 IP 地址转换为 struct in_addr 结构体。
inet_pton() 函数的原型定义在 <arpa/inet.h> 头文件中。其函数原型如下:
int inet_pton(int family, const char *str, void *addr);
参数说明:
- int family: 地址族,通常使用 AF_INET(IPv4)或 AF_INET6(IPv6)。
- const char *str: 要转换的 IP 地址字符串,通常是点分十进制格式的 IPv4 地址(例如:“192.168.1.1”)或者是标准的 IPv6 地址字符串。
- void *addr: 指向 struct in_addr(对于 IPv4)或 struct in6_addr(对于 IPv6)结构的指针,用于存储转换后的地址。
返回值:
- 返回值为 1,表示转换成功。
- 返回值为 0,表示输入字符串不是有效的地址。
- 返回值为 -1,表示发生错误,可以使用 errno 获取错误码。
inet_pton() 是一种更安全和现代的方法,用于将 IP 地址字符串转换为二进制格式,推荐使用,取代了更早期的 inet_addr() 函数。
2.3、listen() 函数
函数原型:
int listen(int sockfd, int backlog);
参数:
- sockfd: socket描述符。
- backlog: 指定等待连接的最大数量。
返回值:成功时返回0,失败时返回-1。
listen()就像一个酒店里的迎宾的人,当客户端连接进来是首先接触到的就是这个listen,然后由将其带到大堂交给服务员来为他提供其他服务。
2.4、accept() 函数
函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd: socket 描述符。
- addr: 可选的 sockaddr 结构指针,用来存储客户端的地址。
- addrlen: 可选的指针,用于表示地址结构的大小。
返回值:成功时返回一个新的 socket 描述符,代表与客户端的连接,失败时返回-1。
在调用这个函数之前,如何没有调用fcntl()函数设置socket fd为非阻塞模式,程序会一直阻塞住,直到有客户端连接进来。
2.5、fcntl() 函数
fcntl() 函数用于在 UNIX 和类 UNIX 系统编程中对文件描述符执行控制操作。fcntl() 提供了多种功能,包括复制文件描述符、改变描述符的属性、获取当前状态等。
fcntl() 函数的原型定义在 <fcntl.h> 头文件中,如下所示:
int fcntl(int fd, int cmd, ... /* arg */ );
参数说明:
- int fd: 要操作的文件描述符,通常是通过 open() 或 socket() 等函数获得的。
- int cmd: 控制命令,决定执行的操作。
- … /* arg */: 可选参数,具体依赖于 cmd 的值。例如,某些命令需要额外的参数来执行特定的操作。
以下是 fcntl() 函数中 int cmd 参数的常用值:
F_DUPFD | 0 | 复制文件描述符,并返回新的文件描述符。 |
F_GETFD | 1 | 获取文件描述符的标志。 |
F_SETFD | 2 | 设置文件描述符的标志。 |
F_GETFL | 3 | 获取文件状态标志。 |
F_SETFL | 4 | 设置文件状态标志。 (O_NONBLOCK, O_APPEND 等) |
F_GETLK | 5 | 获取文件锁定的信息。 |
F_SETLK | 6 | 设置文件锁定(非阻塞)。 |
F_SETLKW | 7 | 设置文件锁定(阻塞)。 |
F_GETOWN | 8 | 获取异步 I/O 所有者。 |
F_SETOWN | 9 | 设置异步 I/O 所有者。 |
F_GETSIG | 10 | 获取异步 I/O 的信号。 |
F_SETSIG | 11 | 设置异步 I/O 的信号。 |
F_GETLK64 | 12 | 获取文件锁定的信息(64 位版)。 |
F_SETLK64 | 13 | 设置文件锁定(64 位版,非阻塞)。 |
F_SETLKW64 | 14 | 设置文件锁定(64 位版,阻塞)。 |
注意:关于文件锁的 F_GETLK, F_SETLK, 和 F_SETLKW 命令需要一个 struct flock 结构体作为可选参数,用于描述锁的类型、偏移量和长度等。F_GETLK64, F_SETLK64, 和 F_SETLKW64 是针对大文件(通常是大于 2GB 的文件)的锁定操作。
返回值:
- 成功时,fcntl() 返回取决于 cmd 的特定值。对于 F_GETFD 和 F_GETFL等命令,这将是所请求的状态标志;对于 F_DUPFD,返回新的文件描述符。
- 失败时,返回 -1,并设置 errno 以指示错误。
对于网络 IO 设置阻塞和非阻塞,主要使用F_GETFL和F_SETFL两个。设置标志时最好要先把之前的标志获取出来,然后通过位运算把网络 IO 模式 “或(|)”进去,不要直接设置,这会把它之前的其他标志全部清除了,是非常不建议直接设置的。
2.6、connect() 函数
函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: socket描述符。
- addr: 指向 sockaddr 结构的指针,包含要连接的服务器地址。
- addrlen: 地址结构的大小。
返回值:成功时返回0,失败时返回-1。
2.7、send() 函数
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
- sockfd: socket描述符。
- buf: 指向要发送的数据的指针。
- len: 要发送的字节数。
- flags: 通常设置为0。
返回值:成功时返回发送的字节数,失败时返回-1。
注意:send()返回一个大于0的数并不意味发送成功,数据并不一定已经发送出去了。send()只是将数据拷贝到协议栈,什么时候发送出去不是我们决定的。要判断发送的数据是否已经被对端接收到,只有一个标准:对端回了确认消息。虽然TCP 有 ACK 机制,但在业务层是感知不到的;特别是进程退出的时候,它很难知道是否发送成功。
2.8、recv() 函数
函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
- sockfd: socket描述符。
- buf: 指向接收数据的缓冲区。
- len: 缓冲区的大小。
- flags: 通常设置为0。
返回值:成功时返回接收到的字节数,失败时返回-1,连接关闭时返回0。
2.9、close() 函数
函数原型:
#include <unistd.h>
int close(int fd);
fd:是需要关闭的文件描述符(在这里是socket描述符)。成功时返回0,失败时返回-1。
三、阻塞和非阻塞的区别
阻塞会一直等待数据到来才会返回。非阻塞会立即返回,不管有没有数据。
那么,在网络编程中accept()函数到底阻塞在哪里?阻塞的条件是什么?
accept() 函数阻塞在 等待新的连接请求 的过程中。accept() 函数的阻塞并非发生在特定的代码行,而是在 内核等待连接请求到达 的过程中。内核会在后台持续监听网络接口,一旦检测到新的连接请求,就会将请求放入连接队列中,并将 accept() 函数从阻塞状态唤醒。
阻塞的条件包括:
没有连接请求 。
连接队列已满。
四、如何开发一个TCP服务器?
开发 TCP 服务器的基本步骤:
下面是一个简单地“一问一答”的服务器示意图: 下面的代码每次只能处理一个客户端的数据,因此也是最基础的“一问一答”的服务器。
服务器端代码:
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#define PORT 8080
#define LINSTEN_BLOCK 20
#define BUFFER_LEN 4096
#define SET_NONBLOCK 0
bool setIoMode(int fd, int mode);
int main(int argc, char**argv)
{
// 1. Create socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == –1) {
std::cout << "socket return " << errno << ", " << strerror(errno) << std::endl;
return –1;
}
// 2. Set the port and bind it.
sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(sockaddr_in));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htons(INADDR_ANY); // bind ip address.
serverAddr.sin_port = htons(PORT); // bind port.
if (bind(listenfd, (sockaddr*)&serverAddr, sizeof(serverAddr)) == –1) {
std::cout << "bind return " << errno << ", " << strerror(errno) << std::endl;
return –2;
}
#if SET_NONBLOCK
// set nonblock mode.
setIoMode(listenfd, O_NONBLOCK);
#endif
// 3. listening port.
if (listen(listenfd, LINSTEN_BLOCK) == –1) {
std::cout << "listen return " << errno << ", " << strerror(errno) << std::endl;
return –3;
}
std::cout << "server listening port " << PORT << std::endl;
while(1) {
// 4. accept connect.
sockaddr_in clientAddr;
memset(&clientAddr, 0, sizeof(clientAddr));
socklen_t clienLen = sizeof(clientAddr);
int clientfd = accept(listenfd, (sockaddr *)&clientAddr, &clienLen);
if (clientfd == –1) {
std::cout << "accept return " << errno << ", " << strerror(errno) << std::endl;
continue;
}
std::cout << "client fd " << clientfd << std::endl;
// 5. send message.
const char *msg = "Hello, Client!";
if (send(clientfd, msg, strlen(msg), 0) == –1) {
std::cout << "send buffer return " << errno << ", " << strerror(errno) << std::endl;
close(clientfd);
continue;
}
// 6. recv message
char buffer[BUFFER_LEN];
if (recv(clientfd, buffer, BUFFER_LEN, 0) == –1) {
std::cout << "recv buffer return " << errno << ", " << strerror(errno) << std::endl;
close(clientfd);
continue;
}
std::cout << "recv buffer: " << buffer << std::endl;
close(clientfd);
}
close(listenfd);
return 0;
}
bool setIoMode(int fd, int mode)
{
int flag = fcntl(fd, F_GETFL, 0);
if (flag == –1) {
std::cout << "fcntl get flags return " << errno << ", " << strerror(errno) << std::endl;
return false;
}
flag |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flag) == –1) {
std::cout << "fcntl set flags return " << errno << ", " << strerror(errno) << std::endl;
return false;
}
return true;
}
客户端代码:
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
char buffer[1024] = {0};
recv(sockfd, buffer, sizeof(buffer), 0);
std::cout << "Message from server: " << buffer << std::endl;
close(sockfd);
return 0;
}
五、总结
以上是Linux C++ socket 编程的一些基本函数及其用法。通过这些函数,你可以实现基本的网络通信,包括建立连接、数据传输等。实际应用中,错误处理、非阻塞和多线程等都会涉及,但这些内容可以在逐步学习中深入了解。
评论前必须登录!
注册