文章目录
- 前言
- 一. 服务器
- 1. 初始化服务器
- 2. 启动服务器
- 二. 客户端
- 三. 多进程服务器
- 结束语
前言
本系列文章是计算机网络学习
的笔记,欢迎大佬们阅读,纠错,分享相关知识。希望可以与你共同进步。
本篇博客基于UDP socket基础,介绍TCP socket编程接口和细节
UDP socket编程可参看【计算机网络学习之路】UDP socket编程
本次编写的服务器和客户端依然是最简单的echo服务器
一. 服务器
服务器的基本框架:
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace ns_server
{
const uint16_t default_port = 8888;
class TcpServer
{
public:
TcpServer(uint16_t port = default_port) : _port(port){}
void InitServer(){}//初始化服务器
void Start(){}//启动服务器
~TcpServer(){}
private:
int _sock; // 监听套接字
uint16_t _port; // 端口号
};
}
tcp_server.cc
#include"tcp_server.hpp"
#include<memory>
using namespace std;
using namespace ns_server;
static void usage(char*argv)
{
cout<<"Usage\n\t"<<argv<<" serverPort"<<endl;
}
int main(int argc,char*argv[])
{
if(argc!=2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]);
unique_ptr<TcpServer> usvr(new TcpServer(echo,port));
usvr->InitServer();
usvr->Start();
return 0;
}
1. 初始化服务器
服务器的初始化,还是一样的
- 创建套接字
- 绑定套接字
void InitServer()
{
// 1.创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "create sock error," << strerror(errno) << std::endl;
exit(1);
}
std::cout << "create listensock success: " << _sock << std::endl;
// 2.绑定套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_sock , (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error," << strerror(errno) << std::endl;
exit(2);
}
}
需要注意的是,socket的第二个参数为SOCK_STREAM
面向字节流
TCP与UDP不同的地方是,TCP是面向连接的,UDP是无连接的
所以TCP还需要listen
返回值:成功返回0,失败返回-1并设置错误码
backlog参数需要在后续TCP详解中学习,先定义大小为32
const int backlog = 32;
void InitServer()
{
// 1.创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "create sock error," << strerror(errno) << std::endl;
exit(1);
}
std::cout << "create listensock success: " << _sock << std::endl;
// 2.绑定套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_sock , (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error," << strerror(errno) << std::endl;
exit(2);
}
// 3.监听
if (listen(_listensock, backlog) < 0)
{
std::cerr << "listen error," << strerror(errno) << std::endl;
exit(3);
}
}
初始化到此就结束了
接下来是启动服务器
2. 启动服务器
TCP通过accept获取客户端连接
sockfd
:socket返回的文件描述符addr
:输入输出型参数,客户端信息的结构体addrlen
:输入输出型参数,结构体大小。注意:需要传入addr的大小
返回值是网络文件描述符
在TCP中,socket返回的网络文件可以理解为连接文件,内部保存了连接信息
而accept是从连接文件中获取连接,然后创建套接字,网络文件。
真正通信的是connect创建的网络文件
我们将私有成员的_sock改为_listensock
void Start()
{
while (true)
{
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
socklen_t len = sizeof(client);
int sock = accept(_listensock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
// 提取客户端信息
std::string clientIp = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";
std::cout << "create sock " << sock << " from " << _listensock << std::endl;
}
}
接下来就可以在connect返回的套接字中读写数据了。
本次使用read
和write
void Start()
{
while (true)
{
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
socklen_t len = sizeof(client);
int sock = accept(_listensock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
// 提取客户端信息
std::string clientIp = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";
std::cout << "create sock " << sock << " from " << _listensock << std::endl;
char buffer[1024];
while (true)
{
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << name << "# " << buffer << std::endl;
std::string responce = buffer;//返回收到的数据
int m = write(sock, responce.c_str(), responce.size());
}
else if (n == 0)
{
// 写端关闭
std::cout << name << " quit,me to" << std::endl;
close(sock);
break;
}
else
{
// 读数据异常
std::cerr << "read error" << std::endl;
break;
}
}
}
}
完整代码:
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace ns_server
{
const uint16_t default_port = 8888;
const int backlog = 32;
class TcpServer
{
public:
TcpServer(func_t func, uint16_t port = default_port) : _port(port), _func(func)
{
}
void InitServer()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "create sock error," << strerror(errno) << std::endl;
exit(1);
}
std::cout << "create listensock success: " << _listensock << std::endl;
// 2.绑定套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error," << strerror(errno) << std::endl;
exit(2);
}
// 3.监听
if (listen(_listensock, backlog) < 0)
{
std::cerr << "listen error," << strerror(errno) << std::endl;
exit(3);
}
}
void Start()
{
while (true)
{
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
socklen_t len = sizeof(client);
int sock = accept(_listensock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
// 提取客户端信息
std::string clientIp = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";
std::cout << "create sock " << sock << " from " << _listensock << std::endl;
char buffer[1024];
while (true)
{
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << name << "# " << buffer << std::endl;
std::string responce = buffer;
int m = write(sock, responce.c_str(), responce.size());
}
else if (n == 0)
{
// 写端关闭
std::cout << name << " quit,me to" << std::endl;
close(sock);
break;
}
else
{
// 读数据异常
std::cerr << "read error" << std::endl;
break;
}
}
}
}
~TcpServer()
{
}
private:
int _listensock; // 监听套接字
uint16_t _port; // 端口号
};
}
PS:上述的服务器是单进程,所以只能同时处理一个客户端,读者可以尝试添加一下多进程,多线程或者线程池
本篇博客最后会贴出多进程的方案
二. 客户端
客户端就不作封装了
最开始也是要创建套接字
然后TCP的客户端需要connect
服务器
sockfd
:socket返回的文件描述符addr
:服务器信息的结构体addrlen
:结构体大小。返回值
:成功返回0,失败返回-1并设置错误码
注意:connect时OS会bind客户端
UDP是在发送数据时才会bind
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
static void usage(char *argv)
{
cout << "Usage:\n\t" << argv << " serverIp serverPort" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
// 1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "create sock error," << strerror(errno) << endl;
exit(SOCKET_ERR);
}
cout<<"create sock sucess:"<<sock<<endl;
// 2. 连接
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);
int cnt = 5; // 记录重连次数
// connect时会bind
while (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
cout << "正在重连...还有" << cnt-- << "次" << endl;
if (cnt <= 0)
{
cerr<<"连接失败"<<endl;
exit(CONNECT_ERR);
}
sleep(1);
}
// 连接成功
string name = "["+serverIp + ":" + to_string(serverPort)+"]";
cout << "connect " << name << " sucess" << endl;
return 0;
}
然后也可以开始读写数据了
完整代码:
tcp_client.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
static void usage(char *argv)
{
cout << "Usage:\n\t" << argv << " serverIp serverPort" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
// 1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "create sock error," << strerror(errno) << endl;
exit(SOCKET_ERR);
}
cout<<"create sock sucess:"<<sock<<endl;
// 2. 连接
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);
int cnt = 5; // 记录重连次数
// connect时会bind
while (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
cout << "正在重连...还有" << cnt-- << "次" << endl;
if (cnt <= 0)
{
cerr<<"连接失败"<<endl;
exit(CONNECT_ERR);
}
sleep(1);
}
// 连接成功
string name = "["+serverIp + ":" + to_string(serverPort)+"]";
cout << "connect " << name << " sucess" << endl;
// 发送消息
while (true)
{
cout << "please enter your message# ";
string message;
getline(cin, message);
int n = write(sock, message.c_str(), message.size());
if (n < 0)
{
cerr << "write error," << strerror(errno) << endl;
break;
}
else if (n == 0)
{
cout << "读端关闭,停止写" << endl;
break;
}
char buffer[1024];
int m = read(sock, buffer, sizeof(buffer) - 1);
if (m > 0)
{
buffer[n] = '\0';
cout<<name<<" echo "<<buffer<<endl;
}
else if (m == 0)
{
// 写端关闭
std::cout << name << " quit,me to" << std::endl;
close(sock);
break;
}
else
{
// 读数据异常
std::cerr << "read error" << std::endl;
break;
}
}
return 0;
}
三. 多进程服务器
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace ns_server
{
const uint16_t default_port = 8888;
const int backlog = 32;
class TcpServer
{
public:
TcpServer(uint16_t port = default_port) : _port(port)
{
}
void InitServer()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "create sock error," << strerror(errno) << std::endl;
exit(1);
}
std::cout << "create listensock success: " << _listensock << std::endl;
// 2.绑定套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error," << strerror(errno) << std::endl;
exit(2);
}
// 3.监听
if (listen(_listensock, backlog) < 0)
{
std::cerr << "listen error," << strerror(errno) << std::endl;
exit(3);
}
}
void Start()
{
//忽略子进程的信号,不需要等待子进程退出(推荐!!!)
signal(SIGCHLD,SIG_IGN);
while (true)
{
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
socklen_t len = sizeof(client);
int sock = accept(_listensock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
std::cout << "create sock " << sock << " from " << _listensock << std::endl;
// 多进程
pid_t id = fork();
if (id < 0)
{
close(sock);
continue;
}
else if (id == 0)
{
//子进程
close(_listensock);//建议关掉不需要的fd
if(fork()>0)
exit(0);//子进程退掉,后续为孙子进程
// 提取客户端信息
std::string clientIp = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
service(sock, clientIp, clientPort);
exit(0);
}
//父进程
//一定关掉不需要的fd,防止fd泄露
close(sock);
//pid_t ret=waitpid(id,nullptr,0);//默认为阻塞等待
//pid_t ret=waitpid(id,nullptr,WNOHANG);//非阻塞
//if(ret==id) std::cout<<"wait "<<id<<" sucess"<<std::endl;
}
}
void service(int sock, std::string &clientIp, uint16_t&clientPort)
{
std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";
char buffer[1024];
while (true)
{
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << name << "# " << buffer << std::endl;
std::string responce = buffer;
int m = write(sock, responce.c_str(), responce.size());
}
else if (n == 0)
{
// 写端关闭
std::cout << name << " quit,me to" << std::endl;
close(sock);
break;
}
else
{
// 读数据异常
std::cerr << "read error" << std::endl;
break;
}
}
}
~TcpServer()
{
}
private:
int _listensock; // 监听套接字
uint16_t _port; // 端口号
};
}
结束语
本篇博客到此结束,感谢看到此处。
欢迎大家纠错和补充
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。