目录
一.创建socket套接字(服务器端)
int socket(int domain, int type, int protocol);
domain:选择你要使用的网络层协议 一般是ipv4,也就是AF_INET
type:选择你要使用的应用层协议,这里我们选择tcp,也就是SOCK_STREAM
protocol:这里我们先设置成0
成功返回文件描述符,失败返回-1
_listen_sockfd = socket(AF_INET,SOCK_STREAM,0);
if (_listen_sockfd < 0)
{
LOG(FATAL, "socket error");
exit(SOCKET_ERROR);
}
LOG(DEBUG, "socket create success, sockfd is : %d\n", _listen_sockfd);
二.bind将port与端口号进行绑定(服务器端)
2.1填充sockaddr_in结构
uint16_t htons(uint16_t hostshort);//将端口号从主机序列转成网络序列
in_addr_t inet_addr(const char *cp);//将ip从主机序列转成网络序列 + 字符串风格ip转成点分十进制ip
uint16_t ntohs(uint16_t netshort);//将端口号从网络序列转成主机序列
char *inet_ntoa(struct in_addr in);//将ip从网络序列转成主机序列 + 点分十进制ip转成字符串风格ip
网络通信:struct sockaddr_in
本地通信:sockaddr_un
16位地址类型表明了他们是网络通信还是本地通信
16位地址类型:sin_family
16位端口号:sin_port
32位ip地址:sin_addr.s_addr
//填充sockaddr_in结构
struct sockaddr_in local;
local.sin_family = AF_INET;//表明是网络通信
local.sin_port = htons(_port);//将主机序列转成网络序列
local.sin_addr.s_addr = inet_addr("0.0.0.0");//将字符串类型的点分十进制ip转成四字节ip,并转成网络序列
2.2bind绑定端口
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:要绑定的socket套接字的文件描述符
struct sockaddr *:包含ip地址+端口号的结构体(类型不一样需要进行强转)
socklen_t addrlen:sockaddr_in结构体的大小
//bind绑定端口
int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error");
exit(BIND_ERROR);
}
LOG(DEBUG, "bind success, sockfd is : %d\n", _listen_sockfd);
三.建立连接
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:要建立连接的socket套接字的文件描述符
backlog: 这个值我们暂时先设置成16,他表示能建立的最多的连接数量
//3.建立连接 tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的
int m = listen(_listen_sockfd, default_backlog);//default_backlog = 16
if (m < 0)
{
LOG(FATAL, "listen error");
exit(_listen_sockfd);
}
LOG(DEBUG, "listen success, sockfd is : %d\n", _listen_sockfd);
四.获取连接
在直接通信之前,我们需要先获取连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:从哪个listen套接字获取连接
addr:数据来源于哪个客户端
addrlen:addr的类型大小
// 4.获取连接 不能直接接受数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listen_sockfd, (sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept error\n");
continue;
}
五..进行通信(服务器端)
5.1接收客户端发送的消息
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:获取连接时得到的socket套接字的文件描述符
buf:缓存区
len:缓存区的大小,单位是字节
flags:暂时设置为0
//5.1接受客户端发送的消息
char inbuffer[1024];
ssize_t n = recv(sockfd, inbuffer, sizeof(inbuffer) - 1,0);
5.2给客户端发送消息
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字的文件描述符
buf:缓存区
len:缓存区的大小,单位是字节
flags:设置为0
//5.2给客户端发送消息
send(sockfd, echo_string.c_str(), echo_string.size(),0);
5.3引入多线程
class ThreadData
{
public:
ThreadData(int fd, InetAddr addr, TcpServer *s):sockfd(fd), clientaddr(addr), self(s)
{}
public:
int sockfd;
InetAddr clientaddr;
TcpServer *self;
};
static void *HandlerSock(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->self->Service(td->sockfd, td->clientaddr);
delete td;
return nullptr;
}
//采用多线程
pthread_t t;
ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);
pthread_create(&t, nullptr, HandlerSock, td); //将线程分离
六.客户端通信
6.1创建socket套接字
//1.创建socket套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
6.2客户端bind问题
客户端不需要显示的bind,os会自动帮你绑定端口号!!!
试想一下,你的手机上有抖音和微信两个客户端小程序,如果抖音客户端bind了8080这个端口,微信也想要bind 8888这个端口,那么这时候就会出现一个问题,一个端口号被两个进程竞争!!!结果就是,抖音和微信不可能同时启动。
所以解决方法就是:tcp client建立连接的时候,OS会自己自动随机的给client进行bind
6.3建立连接
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:socket套接字的文件描述符
addr:携带客户端信息的结构体
addrlen:结构体的大小
//2.建立连接
struct sockaddr_in server;
// 构建目标主机的socket信息
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
6.4进行通信
6.4.1.给服务器发送消息
//3.给服务器发送消息
ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0);
6.4.2.接受服务器发送的消息
//接受服务器发送的消息
ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer)-1, 0);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer<< std::endl;
}
七.效果展示
八.代码展示
6.1TcpServer.hpp
用来进行tcp服务端通信
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "InetAddr.hpp"
#include "Log.hpp"
enum
{
SOCKET_ERROR = 1, // 创建套接字失败
BIND_ERROR, // bind绑定端口失败
_listen_sockfd, //创建listen_sockfd失败
USAGE_ERROR // 启动udp服务失败
};
int default_listen_sockfd = -1;
int default_backlog = 16;
class TcpServer;
class ThreadData
{
public:
ThreadData(int fd, InetAddr addr, TcpServer *s):sockfd(fd), clientaddr(addr), self(s)
{}
public:
int sockfd;
InetAddr clientaddr;
TcpServer *self;
};
class TcpServer
{
public:
TcpServer(uint16_t port) : _port(port), _listen_sockfd(default_listen_sockfd)
{
}
void Init()
{
// 1.创建socket套接字
_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sockfd < 0)
{
LOG(FATAL, "socket error");
exit(SOCKET_ERROR);
}
LOG(DEBUG, "socket create success, sockfd is : %d\n", _listen_sockfd);
// 2.bind将套接字和端口号进行绑定
// 填充sockaddr_in结构
struct sockaddr_in local;
local.sin_family = AF_INET; // 表明是网络通信
local.sin_port = htons(_port); // 将主机序列转成网络序列
local.sin_addr.s_addr = inet_addr("0.0.0.0"); // 将字符串类型的点分十进制ip转成四字节ip,并转成网络序列
// bind绑定端口
int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error");
exit(BIND_ERROR);
}
LOG(DEBUG, "bind success, sockfd is : %d\n", _listen_sockfd);
// 3.建立连接 tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的
int m = listen(_listen_sockfd, default_backlog); // default_backlog = 16
if (m < 0)
{
LOG(FATAL, "listen error");
exit(_listen_sockfd);
}
LOG(DEBUG, "listen success, sockfd is : %d\n", _listen_sockfd);
}
void Service(int sockfd, InetAddr client)
{
LOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);
std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]# ";
while (true)
{
char inbuffer[1024];
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0;
std::cout << clientaddr << inbuffer << std::endl;
std::string echo_string = "[server echo]# ";
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
// client 退出&&关闭连接了
LOG(INFO, "%s quit\n", clientaddr.c_str());
break;
}
else
{
LOG(ERROR, "read error\n", clientaddr.c_str());
break;
}
}
std::cout << "server开始退出" << std::endl;
sleep(10);
close(sockfd); // 文件描述符泄漏
}
static void *HandlerSock(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->self->Service(td->sockfd, td->clientaddr);
delete td;
return nullptr;
}
void Stat()
{
while (true)
{
// 4.获取连接 不能直接接受数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept error\n");
continue;
}
//采用多线程
pthread_t t;
ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);
pthread_create(&t, nullptr, HandlerSock, td); //将线程分离
}
}
~TcpServer()
{
}
private:
uint16_t _port;
int _listen_sockfd;
};
6.2TcpClient.cc
用来进行tcp客户端通信
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
//1.创建socket套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// tcp client 要bind,不要显示的bind.
//2.建立连接
struct sockaddr_in server;
// 构建目标主机的socket信息
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect error" << std::endl;
exit(3);
}
while(true)
{
std::cout << "Please Enter# ";
std::string outstring;
std::getline(std::cin, outstring);
//3.给服务器发送消息
ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0); //write
if(s > 0)
{
char inbuffer[1024];
//接受服务器发送的消息
ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer)-1, 0);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer<< std::endl;
}
else
{
break;
}
}
else
{
break;
}
}
close(sockfd);//防止文件描述符泄漏
return 0;
}
6.3InetAddr.hpp
用来解析request中包含的对方主机的ip地址和prot端口号
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class InetAddr
{
private:
void GetAddress(std::string *ip, uint16_t *port)
{
*port = ntohs(_addr.sin_port);
*ip = inet_ntoa(_addr.sin_addr);
}
public:
InetAddr(const struct sockaddr_in &addr) : _addr(addr)
{
GetAddress(&_ip, &_port);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
6.4LockGuard.hpp
用来对日志信息进行加锁操作
#include <iostream>
#include <pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex); // 构造加锁
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);// 析构释放锁
}
private:
pthread_mutex_t *_mutex;
};
6.5Log.hpp
用来打印日志信息
#pragma once
#include <cstdio>
#include <iostream>
#include <string>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
#include <fstream>
#include "LockGuard.hpp"
bool IsSave = false;//是否向文件中写入
const std::string logname = "log.txt";//日志信息写入的文件路径
// 日志是有等级的
enum Level
{
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
// 将日志的登记由整形转换为字符串
std::string LevelToString(int level)
{
switch (level)
{
case DEBUG:
return "Debug";
case INFO:
return "Info";
case WARNING:
return "Warning";
case ERROR:
return "Error";
case FATAL:
return "Fatal";
default:
return "Unknown";
}
}
// 获取时间
std::string GetTimeString()
{
time_t curr_time = time(nullptr);
struct tm *format_time = localtime(&curr_time);
if (format_time == nullptr)
return "None"; // 没有获取成功
char time_buffer[1024];
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
format_time->tm_year + 1900, // 这里的year是减去1900之后的值,需要加上1900
format_time->tm_mon + 1, // 这里的mon是介于0-11之间的,需要加上1
format_time->tm_mday,
format_time->tm_hour,
format_time->tm_min,
format_time->tm_sec);
return time_buffer;
}
//将日志信息写入到文件中
void SaveFile(const std::string &filename, const std::string &message)
{
std::ofstream out(filename, std::ios::app);
if (!out.is_open())
{
return;
}
out << message;
out.close();
}
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//定义锁 支持多线程
// 日志是有格式的
// 日志等级 时间 代码所在的文件名/行数 日志的内容
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
pid_t selfid = getpid();
char buffer[1024];
va_list arg;//定义一个void* 指针
va_start(arg, format);//初始化指针,将指针指向可变参数列表开始的位置
vsnprintf(buffer, sizeof(buffer), format, arg);//将可变参数列表写入到buffer中
va_end(arg);//将指针置空
std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +
"[" + std::to_string(selfid) + "]" +
"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer + "\n";
LockGuard lockguard(&lock);
if (!issave)
{
std::cout << message;//将日志信息打印到显示器中
}
else
{
SaveFile(logname, message);//将日志信息写入到文件
}
}
// C99新特性__VA_ARGS__
#define LOG(level, format, ...) do{ LogMessage(__FILE__, __LINE__,IsSave,level, format, ##__VA_ARGS__); }while(0)
#define EnableFile() do{ IsSave = true; }while(0)
#define EnableScreen() do{ IsSave = false; }while(0)
6.6main.cc
主函数
#include <iostream>
#include <memory>
#include "TcpServer.hpp"
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;
}
// ./udpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
EnableScreen();
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> usvr = std::make_unique<TcpServer>(port);
usvr->Init();
usvr->Stat();
return 0;
}
6.7makefile
.PHONY:all
all:tcpserver tcpclient
tcpclient:TcpClient.cc
g++ -o tcpclient TcpClient.cc -std=c++14
tcpserver:main.cc
g++ -o tcpserver main.cc -std=c++14 -lpthread
.PHONY:clean
clean:
rm -f tcpserver tcpclient