文章目录
- 一、TCP协议和UDP协议的特点
- 二、网络字节序
- 三、Socket编程常用接口
-
- [1. 创建套接字与绑定IP+端口](#1. 创建套接字与绑定IP+端口)
- [2. UDP收发数据](#2. UDP收发数据)
- [3. TCP建立连接](#3. TCP建立连接)
- 四、UDP实现Echo服务测试
- 五、TCP实现Echo服务测试
本文完整项目代码已提交在我的github仓库中
一、TCP协议和UDP协议的特点
我们了解了TCP/IP协议栈,就明白:在应用层编写网络程序,必须要和传输层交流。传输层属于操作系统内核,我们必须调用相关的系统调用来进行网络通信。
TCP协议和UPD协议是传输层两种最主要的协议:
- TCP(Transmission Control Protocol 传输控制协议),特点是:
- 有连接:通信前必须先和对方建立连接,通信结束必须断开连接
- 可靠传输:它能保证数据不丢包、不乱序、不重复
- 面向字节流:它把数据当成一串连续的字节流来传输,需要应用层自己判断数据的边界。
- UDP(User Datagram Protocol 用户数据协议),特点是:
- 无连接:不需要提前建立连接,直接把数据打包发出去就行。
- 不可靠传输:数据发出去就不管了,它不保证数据一定能送到,也不保证顺序、不保证不重复。
- 面向数据报:UDP每次发送的数据是一个完整的"数据报",接收方收到的一定是一个完整的包。
二、网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件如此,网络数据流也是如此。
TCP/IP协议规定:网络中传递的数据必须是大端!
因此,不管主机是大端还是小端机器,它向网络流中发送数据必须是大端模式的;它从网络流中读取数据也是大端模式的。
例如,如果发送方是小端机器,则它必须先将数据转成大端再发送,反之直接发送即可。
为了使网络程序具有良好的可移植性,同样的程序在大小端机器上都能运行,可以使用以下库函数做网络字节序和主机字节序的转换:
cpp
#include <arpa/inet.h>
// 当前主机序列转为网络序列
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
// 网络序列转为当前主机序列
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
// uint32_t是32位无符号整型类型,uint16_t是16位无符号整型类型
三、Socket编程常用接口
1. 创建套接字与绑定IP+端口
这是UDP和TCP都必须有的两步,必须先创建好一个套接字并且绑定IP和端口号,才能进行通信。
-
socket函数,创建socket文件描述符(向操作系统申请一个套接字):

第一个参数表示你要用哪种网络类型。
AF_INET代表IPv4 网络,AF_INET6代表IPv6网络;第二个参数表示你要用哪种传输方式。
SOCK_STREAM代表TCP。SOCK_DGRAM代表UDP;第三个参数表示协议号,直接写0,系统会自动匹配;
如果创建成功,函数返回创建好的socket文件描述符,否则返回-1。
-
bind函数,向一个已有的socket绑定 IP + 端口,这个函数通常由服务端执行,而客户端不需要:

第一个参数是刚才创建好的空socket文件描述符;
第二个参数是一个结构体,这个结构体需要包含IP和端口号等信息;第三个参数是这个结构体的大小。实际填写这个结构时,我们需要这样写:
cpp
// 假设_socketfd _port已有
struct sockaddr_in local;
bzero(&local, sizeof(local)); //清空local
local.sin_family = AF_INET; //表明网络类型为IPv4
local.sin_port = htons(_port); // 把端口号转为网络序列,记录到local中
local.sin_addr.s_addr = INADDR_ANY; // 表明绑定本主机上所有网卡的ip地址
// 类型强转,两种结构体内存布局完全一样
bind(_socketfd, (struct sockaddr*)local, sizeof(local));
服务端,需要我们手动调用bind函数完成绑定。
但在客户端,需要注意的是:
客户端也一定需要有自己的IP和Port信息
但是,客户端不需要显示地bind自己的IP和Port!
为什么不让客户端显示bind?因为bind port可能和客户端机器上已被使用的port出现冲突!客户端port只需要具有唯一性即可,具体是几,不重要。客户端一般会采用随机端口的方式,由OS自主选择端口自动绑定!
UDP和TCP客户端首次发送数据的时候,OS底层会隐式自动帮你进行获取随机端口,然后自动完成绑定IP和Port工作。
2. UDP收发数据
由于UDP是无连接、不可靠传输,完成上述两步直接收发数据即可。
-
recvfrom函数,UDP从网络中接收数据,获取客户端地址:

sockfd:文件描述符;
buf:接收数据的缓冲区;
len:buf缓冲区大小;
flags:填0即可,表示阻塞读取数据;
src_addr:输出型参数,函数调用结束后,它会自动填入数据发送方的 IP + 端口号。我们也需用
struct sockaddr_in类型变量强转接收。addrlen:src_addr的大小
返回值:读取成功返回读取到的字节数,读取失败返回-1
-
sendto函数,UDP发送数据,必须写明目标IP和端口号。dest_addr参数表明要向谁发送数据,其他参数和recvfrom道理类似。

由于UDP是无连接的,只用read、write函数无法得知对方的IP和端口号。所以UDP中必须使用recvfrom和sendto函数,每次通信都要带着IP+Port信息。
3. TCP建立连接
UDP通信中,创建套接字、绑定ip和port,就可以直接收发数据了,不需要提前建立连接。
但是,TCP通信前,必须先和对方建立连接。这代表TCP在收发数据之前,必须有服务端监听连接请求、客户端发起连接请求的过程!
-
listen函数,TCP服务端开始持续监听是否有发起连接请求:

第一个参数是服务端自己的套接字,第二个参数代表同时发起连接请求的链接队列长度(一般为16或32)
函数调用成功返回0,失败返回-1
-
accept函数,TCP服务端用于接收一个连接请求:

第一个参数是已经处在listen状态的服务端套接字,第二个输出型参数会记录发起连接请求的客户端的IP+Port信息!
值得注意的是,函数调用失败返回-1。函数成功返回一个新的套接字文件描述符,这个fd用于真正和刚才发起连接的客户端进行通信。而原本accept参数的sockfd,可以认为只是一个专门用来监听的套接字!
-
connect函数,TCP客户端用于发起连接请求:

第一个参数是客户端自己的套接字,第二个参数存放服务端的IP+Port,告诉程序要向谁发起连接请求。
走到这里,TCP已经建立了连接,通信双方对象固定,直接read/write函数,或recv/send函数,即可收发数据!
四、UDP实现Echo服务测试
我们使用UDP协议实现一个echo服务:客户端向服务端发送一串内容,服务端收到后什么都不做,再返回给客户端。
服务端:
cpp
// Echo_Server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Logger.hpp"
const static int default_fd = -1;
const static int default_port = 8888;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
};
class UdpServer
{
public:
// 服务端端口号,必须自己设定好。客户端必须提前知道服务器端口号,才能访问
UdpServer(uint16_t port = default_port)
: _port(port),
_sockfd(default_fd)
{}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 第一步: 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4, UDP通信
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
// 第二步: 填充IP和端口号信息
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空local
local.sin_family = AF_INET; // IPv4
local.sin_port = htons(_port); // 写入端口号
local.sin_addr.s_addr = INADDR_ANY; // 当前机器上任意IP地址都计入
// 第三步:bind socket信息
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 强转类型
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
}
void Start()
{
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 读取数据
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
// 网络序列转为主机序列
uint16_t client_port = ntohs(peer.sin_port);
// inet_ntoa函数,能将sin_addr类型的ip地址,转为xx.xx.xx.xx风格字符串,序列也会自动转换
std::string client_ip = inet_ntoa(peer.sin_addr);
std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]# ";
// 客户端发来的数据
inbuffer[n] = 0;
LOG(LogLevel::INFO) << client_address << inbuffer;
std::string echo_string = "server echo# ";
echo_string += inbuffer;
// 向客户端回显数据, peer中记录了客户端是谁
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _sockfd;
uint16_t _port; // 服务端自己设置好,port必须是固定的!因为一定是客户端发起第一次通信,客户端必须提前知道服务端是谁
};
cpp
// Echo_Server_main.cc
#include"Echo_Server.hpp"
#include "Logger.hpp"
#include <memory>
// 执行程序命令行参数为 ./server_udp port
int main(int argc, char *argv[])
{
if(argc != 2)
{
exit(USAGE_ERR);
}
USE_CONSOLE_LOG_STRATEGY();
uint16_t server_port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(server_port);
usvr->Init();
usvr->Start();
return 0;
}
cpp
// Echo_Client_main.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Logger.hpp"
// 客户端必须提前知道服务端的IP和port!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
exit(1);
}
USE_CONSOLE_LOG_STRATEGY();
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(2);
}
// 2. 构建服务端端socket信息
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while(true)
{
std::string message;
// 获取用户输入
std::cout << "Please Enter# ";
std::getline(std::cin, message);
// clinet 发送数据给 server,首次发送即自动bind
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
if(n > 0)
{
char inbuffer[1024] = {0};
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&tmp, &len);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
}
}
return 0;
}
运行:

五、TCP实现Echo服务测试
为了便于后续使用,我们先自己封装好网络地址相关接口:
cpp
// InetAddr.hpp 对网络地址进程描述
#pragma once
#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <string>
#include <strings.h>
#include <sys/socket.h>
class InetAddr
{
public:
InetAddr() = default;
InetAddr(const struct sockaddr_in& address) : _address(address), _len(sizeof(address))
{
_ip = inet_ntoa(_address.sin_addr);
_port = ntohs(_address.sin_port);
}
InetAddr(uint16_t port, const std::string& ip = "0.0.0.0") : _ip(ip), _port(port)
{
// 自动构造struct sockaddr_in 类型网络地址
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port);
_address.sin_addr.s_addr = inet_addr(_ip.c_str());
_len = sizeof(_address);
}
bool operator==(const InetAddr& addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
std::string ToString()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
struct sockaddr* GetNetAddress()
{
return (struct sockaddr*)&_address;
}
socklen_t Len()
{
return _len;
}
~InetAddr()
{
}
private:
struct sockaddr_in _address;
socklen_t _len;
std::string _ip;
uint16_t _port;
};
处理一个通信任务,可以多进程、多线程、线程池...这里以线程池为例,使用我以前自己实现的单例线程池。
代码如下:
cpp
// Echo_Server.hpp
#pragma once
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"
#include <cstdlib>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
using task_t = std::function<void()>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
FORK_ERR
};
static const int gbacklog = 16;
static const uint16_t gport = 8888;
class TcpServer
{
public:
TcpServer(uint16_t port = gport) : _port(port)
{
}
void InitServer()
{
// 1. 创建socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP, 使用SOCK_STREAM选项
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;
// 2. 填充本地socket信息
InetAddr local(_port); // 任意地址bind
// 3. bind
int n = bind(_listensockfd, local.GetNetAddress(), local.Len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success";
// 4. TCP是面向连接的,TCP服务器必须先要处于listen状态。
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen socket success";
}
void Start()
{
while (1)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
// 5. 获取连接
int sockfd = accept(_listensockfd, (struct sockaddr*)&clientaddr, &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errr!";
continue;
}
LOG(LogLevel::INFO) << "accept success, sockfd: " << sockfd;
// 处理通信任务:多进程、多线程、线程池,这里使用线程池
// 将通信任务放入线程池
InetAddr clientaddress(clientaddr);
ThreadPool<task_t>::Instance()->Enqueue([this, sockfd, clientaddress]() -> void
{ this->serviceIO(sockfd, clientaddress); });
}
}
void serviceIO(int sockfd, InetAddr address)
{
// 长连接,长服务
while (1)
{
char inbuffer[1024] = {0};
// 读消息
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;
// 写回消息
std::string echo_string = "server echo# ";
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
break;
}
else
{
LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
break;
}
}
close(sockfd);
}
~TcpServer()
{
close(_listensockfd);
}
private:
uint16_t _port;
// 不需要显示包含ip
int _listensockfd; // 专门用于监听的套接字
};
cpp
// Echo_Client.cc
#include "InetAddr.hpp"
#include <cstdlib>
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::cerr << "./client_tcp server_ip server_port" << std::endl;
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建tcp套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// 客户端不需要显示bind
// 2. 向服务端发起连接
InetAddr serveraddress(server_port, server_ip);
int n = connect(sockfd, (struct sockaddr*)serveraddress.GetNetAddress(), serveraddress.Len());
if (n < 0)
{
std::cerr << "connect to " << serveraddress.ToString() << " failed!" << std::endl;
exit(3);
}
std::cerr << "connect to " << serveraddress.ToString() << " success!" << std::endl;
// 3. 通信
while (1)
{
std::string line;
std::cout << "please Enter# ";
std::getline(std::cin, line);
write(sockfd, line.c_str(), line.size());
char inbuffer[1024];
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
else if (n == 0)
{
std::cout << "read end of file!" << std::endl;
break;
}
else
{
std::cerr << "read error!" << std::endl;
break;
}
}
return 0;
}
cpp
// Echo_Server_main.cc
#include "Echo_Server.hpp"
#include "Logger.hpp"
#include <memory>
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "./server_tcp port" << std::endl;
exit(USAGE_ERR);
}
USE_CONSOLE_LOG_STRATEGY();
uint16_t server_port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(server_port);
tsvr->InitServer();
tsvr->Start();
return 0;
}
效果演示:

本篇完,感谢阅读。