其他
Linux网络编程:TCP与UDP详解
https://juejin.cn/user/13629904404157/posts
Linux网络编程中,TCP和UDP是两种主要的传输层协议。本文将详细分析TCP和UDP在网络编程中的使用、原理、代码示例、数据流动,一些异常情况的处理方式,以及如何使用socket编程实现客户端长连接。
TCP是一种面向连接的协议,它通过三次握手建立连接,然后在连接上进行可靠的数据传输。TCP使用序列号和确认应答(ACK)来保证数据的可靠传输,通过滑动窗口和拥塞控制算法进行流量控制和拥塞控制。
UDP的原理
相比于TCP,UDP是一种更简单的协议。UDP是无连接的,它直接在IP协议之上发送数据报,不提供数据的可靠传输、流量控制或拥塞控制。因此,UDP的延迟和开销较小,适用于对实时性要求高的应用,如语音和视频通信。
数据流动
在TCP和UDP通信中,数据是从客户端流向服务器的。客户端首先建立连接(TCP)或直接发送数据报(UDP),然后服务器接收并处理这些数据,可能会返回响应给客户端。在TCP通信中,数据的流动是双向的,客户端和服务器都可以发送数据和接收数据。在UDP通信中,数据的流动也是双向的,但是由于UDP是无连接的,客户端和服务器可以独立地发送和接收数据。
服务器端
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, 5);
while (true) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
char buffer[1024];
ssize_t read_len = read(client_fd, buffer, sizeof(buffer) - 1);
buffer[read_len] = '\0';
std::cout << "Received: " << buffer << std::endl;
write(client_fd, buffer, strlen(buffer));
close(client_fd);
}
close(server_fd);
return 0;
}
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
int main() {
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
const char *message = "Hello, Server!";
write(client_fd, message, strlen(message));
char buffer[1024];
ssize_t read_len = read(client_fd, buffer, sizeof(buffer) - 1);
buffer[read_len] = '\0';
std::cout << "Received: " << buffer << std::endl;
close(client_fd);
return 0;
}
AF_INET:这是一个地址族(Address Family)常量,表示我们使用的是IPv4协议。在创建套接字时,需要指定地址族以确定使用哪种协议。另一个常见的地址族是AF_INET6,表示使用IPv6协议。 SOCK_STREAM:这是一个套接字类型(Socket Type)常量,表示我们使用的是面向连接的、可靠的字节流。在TCP协议中,我们使用SOCK_STREAM类型的套接字。另一个常见的套接字类型是SOCK_DGRAM,表示无连接的、不可靠的数据报文,通常用于UDP协议。 socket(AF_INET, SOCK_STREAM, 0):这是一个系统调用,用于创建一个新的套接字。它接受三个参数:地址族(如AF_INET)、套接字类型(如SOCK_STREAM)和协议(通常设置为0,让系统自动选择协议,如TCP或UDP)。此函数返回一个套接字文件描述符,用于后续的网络操作。 struct sockaddr_in:这是一个用于表示IPv4套接字地址的结构体。它包含了地址族、端口号和IPv4地址。在网络编程中,我们需要使用此结构体来设置服务器和客户端的地址信息。 server_addr.sin_family = AF_INET:设置sockaddr_in结构体中的地址族字段为AF_INET,表示使用IPv4协议。 server_addr.sin_port = htons(8080):设置sockaddr_in结构体中的端口号字段。htons()函数将主机字节序(Host Byte Order)转换为网络字节序(Network Byte Order)。这里我们设置端口号为8080。 INADDR_ANY:这是一个特殊的IPv4地址(0.0.0.0),表示服务器将监听所有可用的网络接口。当服务器有多个网络接口时,使用INADDR_ANY可以让服务器接受来自任何接口的连接请求。 server_addr.sin_addr.s_addr = INADDR_ANY:设置sockaddr_in结构体中的IPv4地址字段为INADDR_ANY,表示服务器将监听所有可用的网络接口。
服务器端
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
int main() {
int server_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
while (true) {
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
ssize_t read_len = recvfrom(server_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &client_addr_len);
buffer[read_len] = '\0';
std::cout << "Received: " << buffer << std::endl;
sendto(server_fd, buffer, strlen(buffer), 0, (struct sockaddr *)&client_addr, client_addr_len);
}
close(server_fd);
return 0;
}
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
int main() {
int client_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
const char *message = "Hello, Server!";
sendto(client_fd, message, strlen(message), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
char buffer[1024];
struct sockaddr_in recv_addr;
socklen_t recv_addr_len = sizeof(recv_addr);
ssize_t read_len = recvfrom(client_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&recv_addr, &recv_addr_len);
buffer[read_len] = '\0';
std::cout << "Received: " << buffer << std::endl;
close(client_fd);
return 0;
}
在TCP通信中,我们首先需要建立一个TCP连接,然后才能在这个连接上进行数据传输。以下是TCP通信的详细步骤和时序图:
服务器执行socket()函数,创建一个新的套接字。 服务器执行bind()函数,将套接字绑定到一个指定的地址(包括IP地址和端口号)。 服务器执行listen()函数,使套接字进入监听模式,等待客户端的连接请求。 服务器执行accept()函数,阻塞并等待客户端的连接请求。当一个客户端连接请求到来时,accept()函数返回,并创建一个新的套接字与客户端进行通信。 客户端执行socket()和connect()函数,向服务器发起连接请求。connect()函数会发送一个SYN(同步)数据包到服务器。 服务器收到SYN数据包,在accept()函数返回后,回复一个SYN+ACK(确认应答)数据包给客户端。 客户端收到SYN+ACK数据包,回复一个ACK数据包给服务器,完成TCP连接的建立。 TCP连接建立后,客户端和服务器可以通过read()和write()函数进行数据传输。
| |
| socket() |
| |
| bind() |
| |
| listen() |
| |
| accept() |
| |
|--等待客户端连接请求--->|
| |
| |
| socket(), connect() |
|<--- SYN ------------|
| |
|-- SYN + ACK ------->|
| |
|<--- ACK ------------|
| |
|<-- Data ------------|
| read(), write() |
| |
|-- Data -----------> |
| read(), write() |
| |
与TCP不同,UDP是一种无连接的协议,客户端和服务器不需要建立连接就可以直接发送数据。以下是UDP通信的详细步骤:
服务器执行socket()函数,创建一个新的套接字。 服务器执行bind()函数,将套接字绑定到一个指定的地址(包括IP地址和端口号)。 客户端执行socket()函数,创建一个新的套接字。 客户端可以直接通过sendto()函数发送数据到服务器。 服务器通过recvfrom()函数接收客户端发送的数据。
| |
| socket() |
| |
| bind() |
| |
|----等待客户端数据---->|
| |
| |
| socket()|
| sendto()|
|<--- Data -----------|
| recvfrom() |
| |
笔者所在客户端项目的网络层是用C++实现,其中的长连接部分就是用socket接口编程实现的。本节就来看看如何在客户端使用socket和服务器建立连接、读取和发送数据。
下面是使用socket建立连接的实现:
// 检查是否在正确的线程上调用
DCHECK(thread_checker_.CalledOnValidThread());
// 检查连接状态是否为 CONNECT_STATE_NONE
DCHECK(connect_state_ == CONNECT_STATE_NONE);
// 设置连接状态为 CONNECT_STATE_CONNECT
connect_state_ = CONNECT_STATE_CONNECT;
// 设置回调函数
write_callback_ = callback;
before_connect_callback_ = beforeConnectCallback;
// 如果主机名不为空且地址列表为空,则解析主机名
if (hostname_.length() && sockaddrs_.empty()) {
int ret = resolveHostBeforeConnect();
if (ret != 0) return ERR_IO_PENDING;
}
// 遍历地址列表,尝试创建套接字
while (!sockaddrs_.empty()) {
hostaddr_ = sockaddrs_.front();
sockaddrs_.erase(sockaddrs_.begin());
struct sockaddr_storage *addr = (struct sockaddr_storage *)hostaddr_.data();
LOG(WARNING) << "AsyncSocket connect IP: " << Sockaddr2IP((const sockaddr*)addr);
socket_fd_ = socket(addr->ss_family, SOCK_STREAM, IPPROTO_TCP);
if (socket_fd_ < 0) {
// 如果创建套接字失败,根据错误码输出错误日志
switch errno {
case EAFNOSUPPORT:
LOG(ERROR) << "AsyncSocket create socket encounter EAFNOSUPPORT";
break;
case EPROTONOSUPPORT:
LOG(ERROR) << "AsyncSocket create socket encounter EPROTONOSUPPORT";
break;
case ENFILE:
LOG(ERROR) << "AsyncSocket create socket encounter ENFILE";
break;
case EMFILE:
LOG(ERROR) << "AsyncSocket create socket encounter EMFILE";
break;
default:
LOG(ERROR) << "AsyncSocket create socket returned an error, errno=" << errno;
break;
}
} else {
break;
}
}
// 如果套接字创建失败,调用回调函数并返回错误码
if (socket_fd_ < 0) {
if (!beforeConnectCallback.is_null()) beforeConnectCallback.Run(false);
if (!callback.is_null()) callback.Run(ERR_ADDRESS_INVALID);
return socket_fd_;
}
// 设置套接字选项
SetNonBlocking(socket_fd_);
SetTCPNoDelay(socket_fd_, true);
SetTCPKeepAlive(socket_fd_, true, 900);
SetTCPNoSigPipe(socket_fd_);
SetTCPTimeout(socket_fd_, 15);
// 调用连接前的回调函数
if (!beforeConnectCallback.is_null()) beforeConnectCallback.Run(true);
// 尝试连接目标地址
int rv = ::connect(socket_fd_, (sockaddr *)hostaddr_.data(), (socklen_t)hostaddr_.length());
// 将错误码映射为对应的错误类型
int ret = rv == 0 ? OK : MapConnectError(errno);
// 如果连接不需要等待,则直接调用回调函数并返回结果
if (ret != ERR_IO_PENDING) {
callback.Run(ret);
return ret;
}
// 监听套接字的可写事件,以便在连接成功时通知
if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
socket_fd_, true, base::MessageLoopForIO::WATCH_WRITE,
&write_socket_watcher_, this)) {
PLOG(ERROR) << "WatchFileDescriptor failed on connect, errno " << errno;
return errno;
}
return ret;
}
通过这个函数,可以实现异步连接目标服务器,并在连接成功或失败时调用相应的回调。
建立完长连接后,基于socket实现异步读写如下:
int AsyncSocket::read(IOBuffer* buf, size_t buf_len, const CompletionCallback& callback) {
// 检查是否在正确的线程上调用
DCHECK(thread_checker_.CalledOnValidThread());
// 检查读取回调是否为空
CHECK(read_callback_.is_null());
// 不支持同步操作,回调函数不能为空
DCHECK(!callback.is_null());
// 检查缓冲区长度是否大于0
DCHECK_LT(0, buf_len);
// 检查连接状态
if (connect_state_ != CONNECT_STATE_CONNECT_COMPLETE) {
// 如果连接未完成,回调并返回错误
callback.Run(ERR_SOCKET_NOT_CONNECTED);
return 0;
}
// 调用系统read函数读取数据
ssize_t rv = ::read(socket_fd_, buf->data(), buf_len);
// 将返回值或错误码映射为结果
int ret = rv >= 0 ? (int)rv : MapSystemError(errno);
// 如果结果不为0且不为ERR_IO_PENDING,回调并返回结果
if (ret != 0 && ret != ERR_IO_PENDING) {
callback.Run(ret);
return ret;
}
// 监听套接字的可读事件,以便在数据到来时通知
if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
socket_fd_, true, base::MessageLoopForIO::WATCH_READ,
&read_socket_watcher_, this)) {
PLOG(ERROR) << "WatchFileDescriptor failed on read, errno " << errno;
return errno;
}
// 保存缓冲区、长度和回调函数
read_buf_ = buf;
read_buf_len_ = buf_len;
read_callback_ = callback;
// 返回ERR_IO_PENDING表示操作正在进行
return ERR_IO_PENDING;
}
// AsyncSocket::write函数,用于向socket写入数据
int AsyncSocket::write(IOBuffer* buf, size_t buf_len, const CompletionCallback& callback) {
// 检查是否在正确的线程上调用
DCHECK(thread_checker_.CalledOnValidThread());
// 检查连接状态是否为CONNECT_STATE_CONNECT_COMPLETE
DCHECK(connect_state_ == CONNECT_STATE_CONNECT_COMPLETE);
// 检查写入回调是否为空
CHECK(write_callback_.is_null());
// 不支持同步操作,回调函数不能为空
DCHECK(!callback.is_null());
// 检查缓冲区长度是否大于0
DCHECK_LT(0, buf_len);
// 调用WriteWrapper函数写入数据
ssize_t rv = WriteWrapper(socket_fd_, buf->data(), buf_len);
// 将返回值或错误码映射为结果
int ret = rv >= 0 ? (int)rv : MapSystemError(errno);
// 如果结果不为0且不为ERR_IO_PENDING,回调并返回结果
if (ret != 0 && ret != ERR_IO_PENDING) {
callback.Run(ret);
return ret;
}
// 监听套接字的可写事件,以便在可以写入数据时通知
if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
socket_fd_, true, base::MessageLoopForIO::WATCH_WRITE,
&write_socket_watcher_, this)) {
PLOG(ERROR) << "WatchFileDescriptor failed on write, errno " << errno;
return errno;
}
// 保存缓冲区、长度和回调函数
write_buf_ = buf;
write_buf_len_ = buf_len;
write_callback_ = callback;
// 返回ERR_IO_PENDING表示操作正在进行
return ERR_IO_PENDING;
}
通过这两个函数,可以实现异步读取和写入数据。当数据准备好读取或可以写入时,将调用相应的回调函数。这样可以避免阻塞操作,提高程序的性能和响应速度。