Linux网络编程——TcpServer套接字
1、version1
1.1服务端v1
先写一个tcp的服务框架
TcpServer.hpp
#include<iostream>
#include<string>
#include<unistd.h>static const int defaultsockfd = -1;class TcpServer
{
public://构造TcpServer(int port):_port(port),_listensockfd(defaultsockfd),_isrunning(false){}void InitServer(){//创建套接字//构造client端的struct addr_in//bind绑定//tcp还需要另外一步listen(listen所用的套接字就是listensockfd)}bool Loop(){}//析构~TcpServer(){}
private:int _port;int _listensockfd;//命名为listensockfd是有原因的bool _isrunning;
};
main.cc
到时候Server端运行时./tcpserver 8888,只需要端口号就行,ip设置默认绑0.0.0.0也就是任意ip
#include <iostream>
#include<memory>
#include"TcpServer.hpp"void Usage(std::string proc)
{std::cout<<"Usage:\n\t"<<proc<<"local_port\n"<<std::endl;
}
int main(int argc,char *argv[])
{if(argc!=2){Usage(argv[0]);return 1;}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);//定义个tcp类tsvr->InitServer();//初始化tsvr->Start();//启动return 0;
}
实现InitServer函数
创建套接字,函数socket,创建一个套接字成功返回文件描述符,失败返回-1,错误码被设置
domain域(类型)表明网络,man中可选范围如下:一般常用的是AF_UNIX(用于通过文件系统实现的本地套接字,类似于pipe管道)AF_INET(ipv4)和AF_INET6(ipv6)
tpye(服务类型)表明数据报,不止以下两种,TCP就是基于SOCK_STREAM的,UDP基于SOCK_DGRAM
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.
字节流套接字,基于 TCP,提供可靠,有序的服务。
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
数据报套接字,基于 UDP,提供不可靠,无序的服务。
protocol
为套接字特别指定一种要用的传输协议。在给定的协议族中,通常只有一个协议支持特定的套接字类型,在这种情况下,协议可以指定为0。
bind,任意一个服务都要与端口号关联起来,bind就是用来关联端口号的
返回值:
成功返回0,失败返回-1
sockfd可以看作文件信息,struct sockaddr *addr为网络信息,而本地的套接字信息一般为struct sockaddr_in local,bind就是通过sockfd然后将local转为struct sockaddr*然后关联起来从而得到,ip和port
创建套接字、构建本地套接字信息、绑定后就行了吗?
listen,tcp是面向连接的,所以我们还需要进行服务端的监听(所以通信之前,必须先建立连接,服务器是被链接的,当服务端启动,那么就要一直等待(监听)直到客户端来连接)
参数说明:
sockfd:需要设置为监听状态的套接字对应的文件描述符。
backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大。。
返回值说明:
监听成功返回0,监听失败返回-1,同时错误码会被设置。
#include<iostream>
#include<string>
#include<unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include"LOG.hpp"
static const int defaultsockfd = -1;
static const int gbacklog = 16;
enum
{SOCKET_ERROR=1,BIND_ERROR,USAGE_ERROR,LISTEN_ERROR
};
class TcpServer
{
public://构造TcpServer(int port):_port(port),_listensockfd(defaultsockfd),_isrunning(false){}void InitServer(){//创建套接字_listensockfd = socket(AF_INET,SOCK_STREAM,0);if(_listensockfd<0)//创建失败{LOG(FATAL,"socket error\n");exit(SOCK_ERROR);}//构造client端的struct addr_instruct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family = AF_INET;//网络通讯方式为ipv4local.sin_port = htons(_port);//主机序列转网络序列local.sin_addr.s_addr=INADDR_ANY;//0.0.0.0,绑定所有网络接口,无论client通过哪个ip进来都可以访问//bindint n = bind(_listensockfd,(struct sockaddr *)&local,sizeof(local));if(n<0){LOG(FATAL,"bind error\n");exit(BIND_ERROR);}//tcp还需要另外一步listen(listen所用的套接字就是listensockfd)n = listen(_listensockfd,gbacklog);if(n<0){//监听失败报错退出LOG(FATAL,"listen error\n");exit(LISTEN_ERROR);}}bool Start(){_isrunning = true;while(_isrunning){}_isrunning = false;}//析构~TcpServer(){}
private:int _port;int _listensockfd;//命名为listensockfd是有原因的bool _isrunning;
};
现在完成了个残次的初始化,构建了启动函数的雏形(死循环)
服务端启动时不能直接接收,tcp要先获取连接
accept函数
参数说明:
sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。
返回值成功生成一个文件描述符(套接字),那么我们之前socket创建的套接字是用来干什么的呢?而且没连接一个client就生成一个套接字,难不成一直有client连接就一直有fd被创建吗?是的。
举个例子,有个餐厅,门外站这两个拉客的人甲和乙,每当有客人来,而且拉到客人了,甲或者乙就会把门打开请客人进去,并对里面喊:“来个服务员负责这一桌的客人”。
以上情景中我们之前创建的套接字就是甲、乙这些负责拉客的人,他们不关心拉完客后客人的动向,负责给客人递菜单、引路、点菜啥的是服务员的事情。所以listen sockfd只负责监听,而accept创建的sockfd负责发送和接收(注意:accept是从listensockfd中获取连接再生成sockfd)
bool Start(){_isrunning = true;while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer); int sockfd = accept(_listensockfd,(struct sockaddr*)&peer,&len);if(sockfd<0){//LOG(WARNING,"accept error\n");continue;}//Service(sockfd,InetAddr(peer));}_isrunning = false;}
ccept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出(就像拉客的不会因为一批客人不进店就不干了一样),服务端获取连接失败后应该继续获取连接。
然后就可以拿着accept 的套接字来发送和接收了
tcp是面向字节流的服务,可以使用read、write、recv、send来进行发送和接收,服务端只需要接收完客户端的数据再发回去就行,当客户端拿到服务端的响应数据后再将该数据进行打印输出。
这里使用read和write
参数:
fd:特定的文件描述符,表示从该文件描述符中读取数据。
buf:数据的存储位置,表示将读取到的数据存储到该位置。
count:数据的个数,表示从该文件描述符中读取数据的字节数。
返回值:
如果返回值大于0,则表示本次实际读取到的字节个数。
如果返回值等于0,则表示读到了文件结尾,即client退出,关闭连接了。
如果返回值小于0,则表示读取时遇到了错误。
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
buf:需要写入的数据。
count:需要写入数据的字节个数。
返回值:
写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
void Service(int sockfd,InetAddr client){LOG(DEBUG,"get a link,info:%s:%d,sockfd:%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;//输出client信息std::string echo_string="[server echo]#";//server输出界面echo_string += inbuffer;write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0){//读到结尾//client退出&&关闭连接LOG(INFO,"%s is quit\n",clientaddr.c_str());break;}else{ //接收失败LOG(ERROR, "read error");break;}close(sockfd);//预防文件描述符泄露}}bool Loop(){_isrunning = true;while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer); int sockfd = accept(_listensockfd,(struct sockaddr*)&peer,&len);if(sockfd<0){//LOG(WARNING,"accept error\n");continue;}Service(sockfd,InetAddr(peer));}_isrunning = false;}
此时服务端一次只能接收一个客户端,没关系后面慢慢完善,先把客户端写了
1.2客户端
之前我们在UdpServer套接字中提到client需要bind但是不要显示的绑定,让操作系统自行绑定,到时候才不会出现端口冲突。客户端构建完套接字就可以直接与服务端发起连接,连接成功OS底层ip地址回和套接字bind
连接connect函数
参数:
sockfd:特定的套接字,表示通过该套接字发起连接请求。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入的addr结构体的长度。
返回值:
连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。
#include<iostream>
#include<string>
#include <cstring>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>void Usage(std::string a)
{std::cout<"Usage\n\t"<<a<<"ip address"<<" "<<"local port"<<std::endl;
}
int main(int argc,char *argv[])
{if(argc!=3){Usage(argv[0]);//./client ip portexit(1);}std::string server_ip = argv[1];uint16_t server_port = argv[2];//创建套接字int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cerr<<"socket error"<<std::endl;exit(2);}//构建目标主机的sock信息 struct sockaddr_in serverstruct sockaddr_in server;memset(server,0,sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());//发起连接int n = connect(sockfd,(struct sockaddr*)server,sizeof(server));if(n<0){std::cerr<<"connect error"<<std::endl;exit(3);}//连接完就可以通信了while(1){std::cout<<"please Enter#";//客户端界面std::string outstring;getline(std::cin,outstring);//用一下send发送,前3个参数和write一样ssize_t s = send(sockfd,outstring.c_str(),outstring.size(),0);//发送成功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;
}
服务端运行
客户端运行
服务端监听到客户端(日志显示)
现在client发消息,server接收打印并返回
以下是全部代码
TcpServer.hpp
#include<iostream>
#include<string>
#include<strings.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include"LOG.hpp"
#include"InetAddr.hpp"
const static int defaultsockfd = -1;
const static int gbacklog = 16;
enum
{SOCKET_ERROR=1,BIND_ERROR,USAGE_ERROR,LISTEN_ERROR
};
class TcpServer
{
public://构造TcpServer(int port):_port(port),_listensockfd(defaultsockfd),_isrunning(false){}void Service(int sockfd,InetAddr client){LOG(DEBUG,"get a link,info:%s:%d,sockfd:%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;//输出client信息std::string echo_string="[server echo]#";//server输出界面echo_string += inbuffer;write(sockfd,echo_string.c_str(),echo_string.size());}else if(n == 0){//读到结尾//client退出&&关闭连接LOG(INFO,"%s is quit\n",clientaddr.c_str());break;}else{ //接收失败LOG(ERROR, "read error\n");break;}}close(sockfd);//预防文件描述符泄露}void InitServer(){//创建套接字_listensockfd = socket(AF_INET,SOCK_STREAM,0);if(_listensockfd<0)//创建失败{LOG(FATAL,"socket error\n");exit(SOCKET_ERROR);}//构造client端的struct addr_instruct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family = AF_INET;//网络通讯方式为ipv4local.sin_port = htons(_port);//主机序列转网络序列local.sin_addr.s_addr=INADDR_ANY;//0.0.0.0,绑定所有网络接口,无论client通过哪个ip进来都可以访问//bindint n = bind(_listensockfd,(struct sockaddr *)&local,sizeof(local));if(n<0){LOG(FATAL,"bind error\n");exit(BIND_ERROR);}//tcp还需要另外一步listen(listen所用的套接字就是listensockfd)n = listen(_listensockfd,gbacklog);if(n<0){//监听失败报错退出LOG(FATAL,"listen error\n");exit(LISTEN_ERROR);}LOG(DEBUG, "listen success,sockfd is : %d\n", _listensockfd);}void Loop(){_isrunning = true;while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer); int sockfd =::accept(_listensockfd,(struct sockaddr*)&peer,&len);if(sockfd<0){//LOG(WARNING,"accept error\n");continue;}Service(sockfd,InetAddr(peer));}_isrunning = false;}//析构~TcpServer(){if(_listensockfd>=0)close(_listensockfd);}
private:int _port;int _listensockfd;//命名为listensockfd是有原因的bool _isrunning;
};
Main.cc
#include <iostream>
#include<memory>
#include"TcpServer.hpp"void Usage(std::string proc)
{std::cout<<"Usage:\n\t"<<proc<<"local_port\n"<<std::endl;
}
int main(int argc,char *argv[])
{if(argc!=2){Usage(argv[0]);return 1;}EnableScreen();uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);//定义个tcp类tsvr->InitServer();//初始化tsvr->Loop();//启动return 0;
}
mainclient.cc
#include<iostream>
#include<string>
#include <cstring>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include<unistd.h>void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}
int main(int argc,char *argv[])
{if(argc!=3){Usage(argv[0]);//./client ip portexit(1);}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);//创建套接字int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cerr<<"socket error"<<std::endl;exit(2);}//构建目标主机的sock信息 struct sockaddr_in serverstruct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());//发起连接int n = connect(sockfd,(struct sockaddr*)&server,sizeof(server));if(n<0){std::cerr<<"connect error"<<std::endl;exit(3);}//连接完就可以通信了while(1){std::cout<<"please Enter#";//客户端界面std::string outstring;getline(std::cin,outstring);//用一下send发送,前3个参数和write一样ssize_t s = send(sockfd,outstring.c_str(),outstring.size(),0);//发送成功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;
}
InetAddr.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.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;uint16_t _port;std::string _ip;
};
LockGuard.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;
};
LOG.hpp
#pragma once
#include <iostream>
#include <fstream>
#include <cstdio>
#include <string>
#include <ctime>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include "LockGuard.hpp"
bool gIsSave = false;
const std::string logname = "log.txt";//日志文件名
enum Level
{DEBUG=0,INFO,WARNING,ERROR,FATAL
};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();
}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,format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return time_buffer;
}pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 2. 日志是由格式的
// 日志等级 时间 代码所在的文件名/行数 日志的内容
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;va_start(arg, format);vsnprintf(buffer, sizeof(buffer), format, arg);va_end(arg);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +"[" + std::to_string(selfid) + "]" +"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer;LockGuard lockguard(&lock);if (!issave){std::cout << message;}else{SaveFile(logname, message);}// pthread_mutex_lock(&lock); // bug??// std::cout << levelstr << " : " << timestr << " : " << filename << " : " << line << ":" << buffer << std::endl;
}// C99新特性__VA_ARGS__
#define LOG(level, format, ...) \do \{ \LogMessage(__FILE__, __LINE__, gIsSave, level, format, ##__VA_ARGS__); \} while (0)#define EnableFile() \do \{ \gIsSave = true; \} while (0)
#define EnableScreen() \do \{ \gIsSave = false; \} while (0)