linux socket编程之tcp(实现客户端和服务端消息的发送和接收)
目录
一.创建socket套接字(服务器端)
二.bind将port与端口号进行绑定(服务器端)
2.1填充sockaddr_in结构
2.2bind绑定端口
三.建立连接
四.获取连接
五..进行通信(服务器端)
5.1接收客户端发送的消息
5.2给客户端发送消息
5.3引入多线程
六.客户端通信
6.1创建socket套接字
6.2客户端bind问题
6.3建立连接
6.4进行通信
6.4.1.给服务器发送消息
6.4.2.接受服务器发送的消息
七.效果展示
八.代码展示
6.1TcpServer.hpp
6.2TcpClient.cc
6.3InetAddr.hpp
6.4LockGuard.hpp
6.5Log.hpp
6.6main.cc
6.7makefile
一.创建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 = 16if (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); //writeif(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之后的值,需要加上1900format_time->tm_mon + 1, // 这里的mon是介于0-11之间的,需要加上1format_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.ccg++ -o tcpclient TcpClient.cc -std=c++14
tcpserver:main.ccg++ -o tcpserver main.cc -std=c++14 -lpthread
.PHONY:clean
clean:rm -f tcpserver tcpclient