
💡Yupureki:个人主页
✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》
🌸Yupureki🌸的简介:

目录
[1. UDP编程](#1. UDP编程)
[1.1 常用接口](#1.1 常用接口)
[1.1.1 socket() -- 创建套接字](#1.1.1 socket() – 创建套接字)
[1.1.2 bind() -- 绑定地址和端口](#1.1.2 bind() – 绑定地址和端口)
[1.1.3 send() / write() - 数据发送](#1.1.3 send() / write() - 数据发送)
[1.1.4 recv() / read()- 数据接收](#1.1.4 recv() / read()- 数据接收)
[1.2 C++封装UDPServer](#1.2 C++封装UDPServer)
[1.3 客户端和服务器构建](#1.3 客户端和服务器构建)
[2. TCP编程](#2. TCP编程)
[2.1 常用接口](#2.1 常用接口)
[2.2.1 listen() -- 监听连接](#2.2.1 listen() – 监听连接)
[2.2.2 accept() -- 接受连接](#2.2.2 accept() – 接受连接)
[2.2.3 connect() -- 发起连接(客户端使用)](#2.2.3 connect() – 发起连接(客户端使用))
[2.2 C++封装InetAddr](#2.2 C++封装InetAddr)
[2.3 C++封装TCPServer](#2.3 C++封装TCPServer)
[2.4 客户端和服务器构建](#2.4 客户端和服务器构建)
1. UDP编程
UDP(User Datagram Protocol)是无连接的传输层协议
1.1 常用接口
1.1.1 socket() -- 创建套接字
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
功能:创建一个新的套接字,返回文件描述符。
-
参数:
-
domain:协议族,常用AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地通信)。 -
type:套接字类型,常用SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。 -
protocol:通常设为 0,让系统自动选择协议(TCP 或 UDP)。
-
-
返回值 :成功返回非负文件描述符 ,失败返回 -1 并设置
errno。
socket会返回一个文件描述符 ,这个文件描述符指向一个文件缓冲区 ,我们往这个缓冲区输入的数据,会被系统发送到远端主机上。因此socket编程本质就是进程间通信
1.1.2 bind() -- 绑定地址和端口
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能 :将套接字与本地 IP 地址和端口绑定。服务端必须调用,客户端通常不调用(由系统自动分配)。
-
参数:
-
sockfd:socket()返回的描述符。 -
addr:指向sockaddr_in(IPv4)或sockaddr_in6结构的指针,包含 IP 和端口。 -
addrlen:地址结构的长度。
-
-
返回值:0 成功,-1 失败。
sockaddr是一个通用的地址结构体, 它本身不直接使用,而是作为bind()、connect()、accept()等函数的参数类型,允许这些函数接收不同协议族的地址结构。但使用的时候一般是sockaddr_in
cppstruct sockaddr_in { sa_family_t sin_family; // AF_INET in_port_t sin_port; // 端口号(网络字节序) struct in_addr sin_addr; // IPv4 地址(网络字节序) char sin_zero[8]; // 填充,使大小与 sockaddr 一致 };
大小:16 字节(与
sockaddr相同,便于类型转换)。使用时先填充
sockaddr_in,然后强制转换 为(struct sockaddr*)传递给函数。
1.1.3 send() / write() - 数据发送
cpp
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 发送数据,
flags通常为 0。成功返回发送的字节数,失败返回 -1。
1.1.4 recv() / read()- 数据接收
cpp
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 接收数据,成功返回接收的字节数(0 表示对方关闭连接),失败返回 -1。
1.2 C++封装UDPServer
我们用C++封装Socket接口,实现一个简易的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_i
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;
};
1.3 客户端和服务器构建
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;
}
测试:

2. TCP编程
2.1 常用接口
TCP编程使用的接口很多和UDP相似,也有部分不一样
- 服务器绑定套接字后,需要监听(listen) ,当有客户端访问后,需要接受(accept)
- socket 返回的文件描述符的意义不再是把缓冲区的数据发送给远端了 。而是作为监听套接字,启动监听状态 ,当发现有客户端连接后,会使用accpet函数 接受连接,此时返回的文件描述符才指向输入缓冲区
- 客户端需要主动**连接(connect)**客户端
2.2.1 listen() -- 监听连接
cpp
int listen(int sockfd, int backlog);
-
功能:将套接字转为被动监听状态,等待客户端连接。
-
参数:
-
sockfd:已绑定的监听套接字。 -
backlog:未完成连接队列的最大长度(通常设为 5~128)。
-
-
返回值:0 成功,-1 失败。
2.2.2 accept() -- 接受连接
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
功能 :从监听套接字的完成连接队列中取出第一个连接,并创建一个新的套接字用于与客户端通信。
-
参数:
-
sockfd:监听套接字。 -
addr:输出参数,返回客户端的地址信息(可为 NULL)。 -
addrlen:输入输出参数,传入地址结构长度,返回实际长度。
-
-
返回值 :成功返回新的连接套接字描述符,失败返回 -1。
2.2.3 connect() -- 发起连接(客户端使用)
cpp
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:客户端主动连接服务器。
-
参数:
-
sockfd:客户端套接字。 -
addr:服务器地址。 -
addrlen:地址长度。
-
-
返回值:0 成功,-1 失败。
2.2 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;
};
2.3 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;
};
2.4 客户端和服务器构建
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();
}


拓展:
我们目前实现的TCPServer有个很严重的问题:单线程/单进程服务器,只能接受一个客户端的请求并处理数据,当多个客户端同时访问时,无法连接。
因此实际业务中,TCPServer的构建都是多线程/多进程服务器