在前几篇博客中,我们学习了Linux网络编程中的一些概念。从本篇博客开始,我们就正式开始写代码。本篇博客我们将写udp服务器和客户端代码,并实现服务器和客户端通信。这些代码学习成本较高,建议大家多敲几遍。如任何问题,欢迎与我沟通。
UDP协议(User Datagram Protocol,用户数据报协议)和TCP协议(Transmission Control Protocol,传输控制协议)是计算机网络中两种常用的传输层协议,它们在多个方面存在显著的异同。以下是对两者异同点的详细比较:
UDP协议 | TCP协议 | |
---|---|---|
可靠性 | 不提供可靠性保证,不保证数据包的顺序、完整性和不重复。 | 提供可靠的数据传输,通过序列号、确认机制和重传机制确保数据的完整性和有序性。 |
连接性 | 无连接协议,发送数据前不需要建立连接,直接发送数据。 | 面向连接的协议,数据传输前需要建立连接,通过“三次握手”机制确认连接状态。 |
传输效率 | 传输效率高,因为不需要建立连接和维持连接状态,开销小。 | 传输效率相对较低,因为需要建立和维护连接,增加了额外的开销。 |
实时性 | 实时性较好,适用于对实时性要求较高的应用,如在线游戏、视频通话等。 | 实时性较差,因为需要等待连接建立和确认,以及处理重传等机制。 |
数据包大小 | 数据包大小没有限制,但通常受限于网络MTU(最大传输单元)。 | 将数据分割成较小的数据块进行传输,以适应不同的网络环境。 |
拥塞控制 | 不使用拥塞控制,网络拥塞时不会降低发送速率。 | 使用拥塞控制机制,根据网络状况调整发送速率,避免网络拥塞。 |
应用场景 | 适用于对可靠性要求不高,但对实时性要求较高的场景,如流媒体传输、DNS查询等。 | 适用于对可靠性要求较高的场景,如文件传输、网页浏览等。 |
UDP协议和TCP协议在可靠性、连接性、传输效率、实时性、数据包大小和拥塞控制等方面存在显著的差异。选择哪种协议取决于具体的应用场景和需求。如果对数据传输的可靠性要求较高,应选择TCP协议;如果对实时性要求较高,且可以容忍一定的数据丢失,则可以选择UDP协议。在实际应用中,两种协议经常结合使用,以满足不同的网络需求。
不难发现,Udp代码较简单,写起来相对的简单一些,上手较容易。所以我们写使用Udp协议进行通信。
为了使大家更加容易理解。我们按照创建udp服务端的整个过程的先后顺序来进行讲解。最后写出完整的代码。
网络通信必须要申请套接字。申请套接字对应的函数为socket。
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
参数:
①domain
:
domain(协议域/协议族):决定了socket的地址类型。常用的协议族有AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等。在通信中,必须采用与协议族对应的地址。例如,AF_INET决定了要使用IPv4地址(32位)与端口号(16位)的组合。
②type
type(socket类型):指定了socket的类型。常用的socket类型有SOCK_STREAM(流式套接字,用于TCP)、SOCK_DGRAM(数据报套接字,用于UDP)、SOCK_RAW(原始套接字,允许对底层协议如IP或ICMP进行直接访问)等。
③protocol
protocol(协议):通常情况下,可以将其设置为0,让系统自动选择type类型对应的默认协议。
返回值
bind函数在网络编程中扮演着至关重要的角色,它主要用于将一个本地协议地址(包括IP地址和端口号)赋予一个套接字。以下是关于bind函数的详细解释:
#include /* See NOTES */
#include
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数
①sockfd
:这是由socket()函数返回的文件描述符,代表已经创建的套接字。
②addr
:这是一个指向特定协议地址结构的指针,如struct sockaddr_in或struct sockaddr_un,它包含了地址、端口和可能的IP地址信息。
③addrlen
:这是地址结构的长度,通常以字节为单位。对于IPv4,通常使用sizeof(struct sockaddr_in);对于IPv6,使用sizeof(struct sockaddr_in6);对于Unix域套接字,使用sizeof(struct sockaddr_un)。
返回值:
使用场景:
在TCP服务器程序中,bind函数通常用于指定服务器应监听的端口号。服务器在启动时捆绑其众所周知的端口,以便客户端可以连接到它。
对于UDP套接字,bind函数同样用于指定接收数据的端口号。
在Unix域套接字中,bind函数可以用来指定套接字在文件系统中的路径名。
注意事项:
recvfrom函数是一个在POSIX兼容操作系统(如Linux)中用于接收数据的系统调用。它主要用于从指定的套接字接收数据,并适用于面向无连接的协议,如UDP(用户数据报协议)。
#include
#include
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数
①sockfd
:已经创建并绑定的套接字的文件描述符。
②buf
:创建好的一块缓冲区的地址。用来承接从网络中读取到的数据。
③len
:该块缓冲区的大小。
④flags
:读取数据的方式。默认设为0——阻塞式读取。
⑤src_addr
:输出型参数,该结构体里面包含着数据发送方的信息,如port、ip等等。如果不需要这些信息,可以设为null。
⑥‘’addrlen
:该结构体的大小。
返回值
注意事项
sendto函数是一个系统调用,用于将数据从指定的套接字发送到目标地址。它通常用于UDP(用户数据报协议)通信,因为UDP是无连接的,所以sendto函数允许你向一个特定的地址发送数据报,而不需要事先建立连接。
#include
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数
返回值:
sendto函数的返回值是一个long类型的整数,表示发送的字节数。具体返回值有以下几种可能:
需要注意的是,sendto函数不保证数据的可靠传输。也就是说,发送的数据可能会丢失,或者接收方可能无法按照发送的顺序接收数据。如果需要可靠的数据传输,应该使用TCP协议而不是UDP。
此外,在使用sendto函数之前,需要确保已经通过socket函数创建了一个套接字,并且(对于面向连接的套接字类型)已经通过connect函数与目标地址建立了连接(尽管对于UDP,连接通常不是必需的,但也可以通过connect建立默认的目标地址)。同时,也需要确保目标地址是有效的,并且发送的数据缓冲区是正确设置的。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace Server
{
using namespace std;
static const string defaultIp = "0.0.0.0"; //TODO
static const int gnum = 1024;
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};
typedef function<void (string,uint16_t,string)> func_t;
class udpServer
{
public:
udpServer(const func_t &cb, const uint16_t &port, const string &ip = defaultIp)
:_callback(cb), _port(port), _ip(ip), _sockfd(-1)
{}
void initServer()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
// 2. 绑定port,ip(TODO)
// 未来服务器要明确的port,不能随意改变
struct sockaddr_in local; // 定义了一个变量,栈,用户
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 你如果要给别人发消息,你的port和ip要不要发送给对方
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->uint32_t 2. htonl(); -> inet_addr
//local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind,服务器的真实写法
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n == -1)
{
cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
// UDP Server 的预备工作完成
}
void start()
{
// 服务器的本质其实就是一个死循环
char buffer[gnum];
for(;;)
{
// 读取数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //必填
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
// 1. 数据是什么 2. 谁发的?
if(s > 0)
{
buffer[s] = 0;
string clientip = inet_ntoa(peer.sin_addr); //1. 网络序列 2. int->点分十进制IP
uint16_t clientport = ntohs(peer.sin_port);
string message = buffer;
cout << clientip <<"[" << clientport << "]# " << message << endl;
// 我们只把数据读上来就完了吗?对数据做处理
_callback(clientip, clientport, message);
}
}
}
~udpServer()
{
}
private:
uint16_t _port;
string _ip; // 实际上,一款网络服务器,不建议指明一个IP
int _sockfd;
func_t _callback; //回调
};
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace Client
{
using namespace std;
class udpClient
{
public:
udpClient(const string &serverip, const uint16_t &serverport)
: _serverip(serverip), _serverport(serverport), _sockfd(-1), _quit(false)
{
}
void initClient()
{
// 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
cout << "socket success: "
<< " : " << _sockfd << endl;
// 2. client要不要bind[必须要的],client要不要显示的bind,需不需程序员自己bind?不需要!!!
// 写服务器的是一家公司,写client是无数家公司 -- 由OS自动形成端口进行bind!-- OS在什么时候,如何bind
}
static void *readMessage(void *args)
{
int sockfd = *(static_cast<int *>(args));
pthread_detach(pthread_self());
while (true)
{
char buffer[1024];
struct sockaddr_in temp;
socklen_t temp_len = sizeof(temp);
size_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &temp_len);
if (n >= 0)
buffer[n] = 0;
cout << buffer << endl;
}
return nullptr;
}
void run()
{
pthread_create(&_reader, nullptr, readMessage, (void *)&_sockfd);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
server.sin_port = htons(_serverport);
string message;
char cmdline[1024];
while (!_quit)
{
//cerr << "# "; // ls -a -l
// cin >> message;
fprintf(stderr, "Enter# ");
fflush(stderr);
fgets(cmdline, sizeof(cmdline), stdin);
cmdline[strlen(cmdline)-1] = 0;
message = cmdline;
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
}
~udpClient()
{
}
private:
int _sockfd;
string _serverip;
uint16_t _serverport;
bool _quit;
pthread_t _reader;
};
} // namespace Client
客户端需要绑定端口号吗?客户端需要显式的绑定端口号吗?
端口号是需要绑定端口号的,但是不需要显式的绑定端口号的。绑定端口号的工作交给操作系统自主完成,这个工作由操作系统在客户端初次发送消息时完成。
相对于服务端来说,客户端必须绑定特定的端口号,但是端口号的数值对于客户端来说就显得不太重要。
服务端必须指定特定的端口号以供客户端根据该端口号来向服务端发送消息。但是客户端而言,如果显式指明端口号,必然会出现两个客户端竞争一个端口号的情况。所以在通信时就由操作系统随机分配一个端口号供客户端进行通信。