UDP编程
常用接口
socket () - 创建套接字
头文件:#include <sys/socket.h>
cpp
int socket(int domain, int type, int protocol);
- 功能:生成一个全新套接字,返回套接字对应的文件描述符,是所有 TCP/UDP 网络程序的起始接口,客户端、服务端程序都需要率先调用该函数。
- 参数说明
- domain:协议族,常用
AF_INET(IPv4 互联网通信)、AF_INET6(IPv6 通信)、AF_UNIX(本机进程本地通信); - type:套接字类型,
SOCK_STREAM对应 TCP 流式套接字、SOCK_DGRAM对应 UDP 数据报套接字; - protocol:一般赋值为 0,操作系统会根据 domain 与 type 自动匹配对应的底层通信协议。
- domain:协议族,常用
- 返回值 :创建成功返回非负的文件描述符;创建失败返回
-1,并且自动设置全局错误标识errno。 - 底层原理 socket 函数返回的文件描述符会关联内核缓冲区,应用向该缓冲区写入的数据,由操作系统完成协议封装后经由网卡发送至远端主机,因此 socket 编程本质是跨主机的进程间通信。
bind () - 绑定套接字
头文件:#include <sys/types.h>、#include <sys/socket.h>
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:将创建的套接字与本机特定 IP 地址和端口号绑定,为服务器程序确立固定的通信地址标识,让客户端能够通过该地址发起连接请求,是 TCP/UDP 服务器初始化的关键步骤之一。
-
参数说明
- sockfd:由
socket()函数返回的套接字文件描述符,唯一标识要绑定的套接字; - addr:指向通用地址结构体
sockaddr的指针,实际传入时通常使用struct sockaddr_in(IPv4)或struct sockaddr_in6(IPv6)结构体并强制类型转换,其中包含要绑定的 IP 地址和端口号(端口号需通过htons转换为网络字节序); - addrlen:地址结构体的字节长度,一般通过
sizeof运算符获取实际传入的地址结构体大小。
- sockfd:由
-
返回值 :绑定成功返回
0;绑定失败返回-1,并设置全局错误标识errno,常见错误包括端口被占用(EADDRINUSE)、地址不可用(EADDRNOTAVAIL)等。 -
底层原理 bind 函数本质是向操作系统内核注册套接字的网络标识,内核会记录该套接字与特定 IP + 端口的映射关系,后续网络数据包到达时,内核可通过目标地址快速定位到对应的套接字并交付数据。对于服务器程序,绑定固定端口是必要操作;而客户端程序通常无需显式调用 bind,由内核自动分配临时端口。
send () /write () - 数据发送
cpp
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 功能:向已建立连接的套接字缓冲区写入数据,由内核完成后续封装与网络发送;write 是 POSIX 通用写接口,在 socket 场景下作用和 flags=0 的 send 完全一致。
- 参数 :
- sockfd:通信套接字文件描述符,TCP 使用 accept 返回的新 fd、connect 后的客户端 fd。
- buf:指向待发送数据缓冲区的指针,存放要发送的二进制数据。
- len:期望发送的数据字节长度。
- flags:控制选项,日常编程默认填 0,使用常规阻塞发送逻辑。
- 返回值:成功返回实际发送出去的字节数;失败返回 - 1 并设置 errno。
recv () /read () - 数据接收
cpp
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 功能:从套接字内核接收缓冲区读取远端发来的数据;read 等价于 flags=0 的 recv,是通用文件读接口。
- 参数 :
- sockfd:用来接收数据的通信套接字描述符。
- buf:应用层缓冲区,用来存放读到的数据。
- len:缓冲区最大可读取字节长度。
- flags:接收控制标识,常规业务统一传 0。
- 返回值: 成功返回本次实际读到的字节数; 返回 0 代表对端主动关闭 TCP 连接; 读取出错返回 - 1 并填充 errno 错误码。
C++封装UDPServer
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#define DEFAULT_PORT 8080
#define MAXNUM 1024
class UdpServer
{
public:
UdpServer(uint16_t port = DEFAULT_PORT)//指定本地端口号
:_port(port)
{}
void init()//初始化->1.创建套接字 2.绑定套接字
{
//创建套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd < 0)
exit(1);
//初始化sockaddr_in
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
//绑定套接字
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n != 0)
exit(1);
}
void start()//服务器启动
{
char buffer[MAXNUM];
//客户端的信息
struct sockaddr_in peer;
socklen_t len = 0;
while(1)
{
char buffer[MAXNUM];
//接受数据
int n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0';
std::cout<<buffer<<std::endl;
}
}
}
private:
uint16_t _port;
int _sockfd = -1;
};
客户端和服务器构建
client.cpp:
- 启动客户端时需要知道我们服务器的ip地址和端口号,这样才能知道给哪个服务器发送数据
- 如果客户端和服务器都是本地启动的,客户端填的ip地址可以是127.0.0.1 ,表示本地环回地址,只在本地查找对应的端口号
- 客户端和服务器都需要创建套接字并绑定
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
int main(int argc,char* argv[])//指定服务器的ip地址和端口号
{
if(argc != 3)
{
std::cout<<"format:"<<__FILE__<<" ip port"<<std::endl;
exit(1);
}
//创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
//初始化sockaddr_in
struct sockaddr_in sock;
bzero(&sock,sizeof(sock));
uint16_t port = atoi(argv[2]);
sock.sin_family = AF_INET;
sock.sin_port = htons(port);
sock.sin_addr.s_addr = inet_addr(argv[1]);
//启动客户端:本地键盘输入数据,传给服务器
while(1)
{
std::cout<<"Enter->";
std::string s;std::getline(std::cin,s);
ssize_t n = sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&sock,sizeof(sock));
}
return 0;
}
server.cpp:
- 服务器启动时需要指定本地端口号
cpp
#include "server.hpp"
int main(int argv,char* argc[])
{
if(argv != 2)
{
std::cout<<"format: "<<__FILE__<<" port"<<std::endl;
exit(1);
}
uint16_t port = atoi(argc[1]);
UdpServer udp(port);
udp.init();
udp.start();
return 0;
}
测试:

TCP编程
常用接口
listen() - 连接
cpp
int listen(int sockfd, int backlog);
作用 :将套接字设置为被动监听模式,等待客户端连接。
参数:
sockfd:已绑定的套接字backlog:内核等待连接的队列最大长度- 注意:仅服务器调用,listen 不阻塞,只是开启监听状态。
accept() - 接受连接
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
作用 :从监听队列中取出一个客户端连接,创建新的通信套接字。
参数:
sockfd:监听套接字addr:输出参数,存储客户端的 IP 和端口addrlen:输出参数,地址结构体长度关键:
- 默认阻塞等待,直到有客户端连接
- 返回值是新的套接字,专门用于和该客户端收发数据
- 原监听套接字继续等待新连接
connect()- 发起连接(客户端使用)
cpp
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用 :客户端主动向服务器发起 TCP 连接(三次握手)。
参数:
sockfd:客户端套接字addr:目标服务器的 IP 和端口addrlen:结构体长度注意:仅客户端调用,连接失败会直接返回 - 1。
C++封装InetAddr
网络编程中,我们需要频繁进行网络字节序和主机字节序之间的转换,频繁使用sockaddr_in
因此为了方便,我们设计一个InetAddr类 ,其中包含代表网络字节序的sockaddr_in,和本地字节序的ip地址和port端口号
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <cstring>
class InetAddr{
public:
InetAddr(struct sockaddr_in & addr)//网络字节序转主机字节序
:_addr(addr)
{
_port = ntohs(addr.sin_port);
char buffer[64];
inet_ntop(AF_INET,&addr.sin_addr,buffer,sizeof(_addr));
_ip = buffer;
}
InetAddr(std::string ip,uint16_t port)//主机字节序转网络字节序
:_port(port),_ip(ip)
{
memset(&_addr,0,sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);
}
InetAddr(uint16_t port)//默认本地环回
:_port(port),_ip("00.00.00.00")
{
memset(&_addr,0,sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
_addr.sin_addr.s_addr = INADDR_ANY;
}
const struct sockaddr_in& get_addr()
{
return _addr;
}
struct sockaddr_in* get_addr_ptr()
{
return &_addr;
}
std::string get_string()
{
return _ip + ":" + std::to_string(_port);
}
std::string get_ip()
{
return _ip;
}
uint16_t get_port()
{
return _port;
}
private:
uint16_t _port;
std::string _ip;
struct sockaddr_in _addr;
};
C++封装TCPServer
在封装TCPServer前,我们需要清楚一个概念:行使一个业务的服务器只能有一个,不能复制。因此我们设计一个类,这个类作为基类,不允许被复制,使得他的派生类也不能复制。我们把这个类放进comm.hpp。同时为了代码的正规性,我们设计统一的退出码
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "InetAddr.hpp"
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <string>
#define CONV(x) ((struct sockaddr*)(&x))
#define DEFAULT_BACKLOG 8
#define MAXNUM 1024
enum ExitCode{
SOCKET = 0,
BIND,
LISTEN,
ACCEPT,
FORMAT,
CONNECT,
};
class nocopy{
public:
nocopy()
{}
~nocopy()
{}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
server.hpp:
这个TCPServer我们期望他实现:
- 初始化和绑定socket套接字
- 设置监听状态
- 接受客户端的连接请求
- 连接成功后,使用回调函数,实现对应的业务
cpp
#pragma once
#include "com.hpp"
class TcpServer : public nocopy
{
private:
using func_t = void(*)(int ,InetAddr&, bool&);
public:
TcpServer(uint16_t port,func_t func)
:_port(port),_func(func)
{}
void init()
{
_listensockfd = socket(AF_INET,SOCK_STREAM,0);
if(_listensockfd < 0)
exit(ExitCode::SOCKET);
InetAddr addr(_port);
int n = bind(_listensockfd,CONV(addr.get_addr()),sizeof(struct sockaddr_in));
if(n < 0)
exit(ExitCode::BIND);
n = listen(_listensockfd,DEFAULT_BACKLOG);
if(n < 0)
exit(ExitCode::LISTEN);
}
void run()
{
_isrunning = true;
while(_isrunning)
{
struct sockaddr_in tmp;
memset(&tmp,0,sizeof(tmp));
socklen_t len = sizeof(tmp);
int fd = accept(_listensockfd,CONV(tmp),&len);
if(fd < 0)
exit(ExitCode::ACCEPT);
InetAddr peer(tmp);
_func(fd,peer,_isrunning);//回调函数
}
}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning = false;
func_t _func;
};
客户端和服务器构建
client.cpp:
- 客户端需要主动连接服务器(未连接成功前,不可发送数据)
cpp
#include "com.hpp"
int main(int argv,char* argc[])
{
if(argv != 3)
{
std::cout<<"format: "<<"./"<<__FILE__<<" ip port"<<std::endl;
exit(ExitCode::FORMAT);
}
InetAddr addr(argc[1],atoi(argc[2]));
std::cout<<"user:"<<addr.get_string()<<std::endl;
int sockfd = socket(AF_INET,SOCK_STREAM,0);
socklen_t len = sizeof(struct sockaddr_in);
ssize_t n = connect(sockfd,CONV(addr.get_addr()),len);
if(n < 0)
exit(ExitCode::CONNECT);
while(1)
{
std::cout<<"Enter->";
std::string s;std::cin>>s;
int n = write(sockfd,s.c_str(),sizeof(s));
if (n < 0)
continue;
}
return 0;
}
server.cpp:
- 服务器需要设置好回调函数
cpp
#include "server.hpp"
void func(int fd,InetAddr &addr, bool &isrunning)
{
while (isrunning)
{
char buffer[MAXNUM];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n < 0)
continue;
else if(n == 0)
break;
buffer[n] = '\0';
std::cout<<"client say: "<<buffer<<std::endl;
}
}
int main(int argv,char* argc[])
{
if(argv != 2)
{
std::cout<<"format: "<<"./"<<__FILE__<<" port"<<std::endl;
exit(ExitCode::FORMAT);
}
uint16_t port = atoi(argc[1]);
TcpServer tsvr(port, func);
tsvr.init();
tsvr.run();
}
