TCP网络套接字
引言
前面我们已经介绍了udp套接字的相关编写,本文主要介绍TCP套接字的相关接口和一些相关知识,方便大家后续对TCP的进一步理解。
相关接口
1、socket
这个接口我们已经在前面有所了解,这里我们再重新回顾一下
第一个参数我们依然使用AF_INET,表示使用网络通信,第二个参数我们需要使用SOCK_STREM,这里和UDP套接字稍微有点区别,需要注意下。最后一个参数我们依旧填零即可,OS会自动识别创建套接字所使用的协议。
2、bind
这个接口我们在前面也已描述,这里简单回顾一下。
在TCP套接字的编写中,绑定套接字依旧是不可或缺的一步,所以我们只需要像写UDP套接字一样,绑定特定的sockfd即可。
注意:在服务端的sockaddr_in 结构中,是不需要绑定特定的IP
3、listen
在了解这个接口之前,我们需要了解一个知识
TCP是面向连接的,所以在通信之前,就必须先建立连接,而服务器是被连接的,一旦tcpserver 启动,首先要做的就是一直等待客户的到来。
所以我们要让绑定的套接字进入监听状态,检测客户端发起地连接,而listen接口就是将套接字设置成监听状态。
下面介绍一下参数:第一个参数sockfd,就是需要设置成监听状态的文件描述符;第二个参数表示全连接队列长度,这涉及OS内核的相关知识,这里后面介绍。该参数不要设置地太大,也不要太小,设成十几即可。
4、accept
在TCP通信时,我们没法直接获取数据,需要先获取连接后,才能获得数据。而accep接口就是用于获取连接地。
其中,第一个参数就是我们之前监听的sockfd。第二个和第三个参数相当于recvfrom的最后两个参数,属于输出型参数,用于获取客户端的相关数据。
这里需要着重介绍一下accept的返回值,如果accept获取连接成功,那么就会新创建一个文件描述符,该文件描述符会被返回。如果创建失败,则返回-1,错误码被设置。每一个客户端来连接服务端时,一旦服务端成功accept到连接,就会创建一个新的文件描述符。
下面介绍一下sockfd和这些新创建的fd之间的关系。
在日常的生活中,我们可能要经常坐火车、飞机到各个地方,在飞机站和火车站对面,我们出站后,常常会看见饭店门口有一些服务员在拉客。当你刚好想要吃饭时,拉客的服务员就会将你带进店内,此时带你进店的服务员会重新出去拉客,让别的服务员招待你。这里拉客的服务员就像sockfd,他只提供拉客服务,具体的服务事项让店内其他服务员负责。这里拉客的服务员就是sockfd,只负责监听和获取连接的服务。而其他的fd就负责具体的业务处理。
5、connect
该接口用于客户端向服务端发起连接,连接成功后,我们这里的客户端会在底层自动绑定一个端口。
第一个参数为socket接口创建的sockfd,第二和第三个参数是我们要连接的服务端参数。
如果连接成功返回“0”,失败返回“-1”。
示例代码
1、TCPserver.hpp
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include <unistd.h>
#include <functional>
#include "InetAddr.hpp"
class TcpServer;
class ThreadDate
{
public:
~ThreadDate()
{
}
ThreadDate(int _sockfd, struct sockaddr_in _add, TcpServer *_self) : sockfd(_sockfd), addr(_add), self(_self)
{
}
public:
TcpServer *self;
int sockfd;
struct sockaddr_in addr;
};
using task_t = std::function<void *(void *)>;
using fun = std::function<std::string(std::string)>;
class TcpServer
{
const static int defaultfd = -1;
const int backlog = 16;
public:
TcpServer(int port, fun _function) : _port(port), fun_t(_function), _listensockfd(defaultfd), _isrunning(false)
{
}
void Init_Server()
{
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(FATAL, "socket create failed")
}
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_port = htons(_port);
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_family = AF_INET;
int n = bind(_listensockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
LOG(FATAL, "bind failed")
exit(1);
}
LOG(INFO, "bind success")
n = ::listen(_listensockfd, backlog);
if (n < 0)
{
LOG(FATAL, "listen failed")
}
}
void Server(int sockfd, struct sockaddr_in &peer)
{
Inetaddr addr(peer);
std::string clientaddr = "[" + addr.Ip() + " : " + std::to_string(addr.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;
}
sleep(5);
break;
}
// while (true)
// {
// if(CheckClose(sockfd,peer))
// {
// LOG(INFO,"client close")
// ::close(sockfd);
// return;
// }
// ssize_t count = 1024;
// std::string result;
// while (true)
// {
// char buffer[1024];
// memset(buffer, 0, sizeof(buffer));
// count = read(sockfd, buffer, sizeof(buffer) - 1);
// if (count > 0)
// {
// // std::cout << client_infor << buffer << std::endl;
// // fflush(stdout);
// result += buffer;
// }
// else if (count == 0)
// {
// // LOG(INFO, "client close...")
// //::close(sockfd);
// break;
// }
// else
// {
// LOG(ERROR, "read fail")
// }
// }
// std::string Re = fun_t(result);
// write(sockfd, Re.c_str(), count);
// }
}
static void *Hand_task(void *arg)
{
ThreadDate *thread = static_cast<ThreadDate *>(arg);
thread->self->Server(thread->sockfd, thread->addr);
return nullptr;
}
void Loop()
{
_isrunning = true;
while (true)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sockfd = ::accept(_listensockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept fail")
// LOG(INFO, "accept success")
// pid_t id = fork();
// if (id == 0)
// {
// ::close(_listensockfd);
// pid_t rid = fork();
// if (rid == 0)
// {
// Server(sockfd, peer);
// }
// exit(0);
// }
// waitpid(id, nullptr, 0);
// ::close(sockfd);
// }
}
else
{
pthread_t thread;
ThreadDate *th = new ThreadDate(sockfd, peer, this);
pthread_create(&thread, nullptr, Hand_task, th);
}
}
_isrunning = false;
}
~TcpServer()
{
::close(_listensockfd);
}
private:
int _listensockfd;
int _port;
bool _isrunning;//表示是否运行
fun fun_t;
};
2、TCPclient.cc
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"
void Usage()
{
std::cout << "./client ip port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
return 1;
}
struct sockaddr_in client;
bzero(&client, sizeof(client));
client.sin_addr.s_addr = inet_addr(argv[1]);
client.sin_port = htons(std::stoi(argv[2]));
client.sin_family = AF_INET;
socklen_t len = sizeof(client);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
LOG(FATAL, "socket fail")
return 1;
}
int rcount = connect(sockfd, (struct sockaddr *)&client, len);
if (rcount != 0)
{
LOG(FATAL, "connect fail....")
return 1;
}
LOG(INFO, "connect success")
while (true)
{
std::string message;
printf("please enter: ");
std::getline(std::cin, message);
ssize_t n = ::send(sockfd, message.c_str(), message.size(), 0);
if (n > 0)
{
printf("[server] : ");
char buffer[1024];
memset(buffer,0, sizeof(buffer));
ssize_t rnum = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (rnum < 0)
{
LOG(ERROR, "recv fail")
continue;
}
if (rnum == 0)
{
break;
}
std::cout << buffer;
}
else
{
LOG(ERROR, "send fail")
break;
}
std::cout << std::endl;
}
::close(sockfd);
return 0;
}
3、TCPserver.cc
#include "TcpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;
}
// ./tcpserver port
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);
tsvr->InitServer();
tsvr->Loop();
return 0;
}
相关文件
InetAddr.hpp
#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;
}
bool operator == (const InetAddr &addr)
{
// if(_ip == addr._ip)
if(_ip == addr._ip && _port == addr._port) // 方便测试
{
return true;
}
return false;
}
struct sockaddr_in Addr()
{
return _addr;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>
#include <pthread.h>
#include <fstream>
enum Level
{
INFO = 0,
DEBUG,
WARNING,
ERROR,
FATAL
};
std::string Level_tostring(int level)
{
switch (level)
{
case INFO:
return "INFO";
case DEBUG:
return "DEBUG";
case WARNING:
return "ERROR";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "Unkown";
}
}
pthread_mutex_t _glock = PTHREAD_MUTEX_INITIALIZER;
bool _is_save = false;
const std::string filename = "log.txt";
void SaveLog(const std::string context)
{
std::ofstream infile;
infile.open(filename,std::ios::app);
if(!infile.is_open())
{
std::cout << "open file failed" << std::endl;
}
else
{
infile << context;
}
infile.close();
}
std::string Gettime()
{
time_t cur_time = time(NULL);
struct tm *time_data = localtime(&cur_time);
if (time_data == nullptr)
{
return "None";
}
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d",
time_data->tm_year + 1900,
time_data->tm_mon + 1,
time_data->tm_mday,
time_data->tm_hour,
time_data->tm_min,
time_data->tm_sec);
return buffer;
}
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
std::string levelstr = Level_tostring(level);
std::string time = Gettime();
// 可变参数
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
std::string context = "[" + levelstr + "]" + "[" + time + "]" + "[" + "line : " + std::to_string(line) + "]" + "[" + filename + "]" + ": " + buffer;
pthread_mutex_lock(&_glock);
if(!issave)
{
std::cout << context << std::endl;
}
else{
SaveLog(context);
}
pthread_mutex_unlock(&_glock);
}
#define LOG(level, format, ...) \
do \
{ \
LogMessage(__FILE__, __LINE__, _is_save, level, format, ##__VA_ARGS__); \
} while (0);
#define EnableFile() \
do \
{ \
_is_save = true; \
} while (0);
#define EnableScreen() \
do \
{ \
_is_save = false;\
} while (0);
相关代码问题
1、如何处理多个请求
在TcpServer中,我是用了多线程处理请求,如果我们使用单线程处理请求,很容易导致一个请求一直在循环等待,其他请求根本得不到处理的情况出现。所以这里我们需要将请求的处理函数设置成多进程或者是多线程的,由于这里转发消息属于一种短时服务,所以我们可以优先使用多线程,当然也可以使用多进程。
多线程代码比较简单,这里不详细介绍,而多进程想要对请求进行灵活地处理,就必须对文件描述符进行合理地管理。这里我们可以创建多个进程,让每个进程管理一个fd。其中父进程我们可以让其管理listensockfd,然后把其他请求生成的fd关闭,这样可以避免误操作,然后让孙子进程对请求生成地fd进行管理。父进程必须要对子进程进行wait,但是这样就会造成一个问题,每次循环结束后,子进程直接被回收了,请求生成的fd直接被关闭了。为了解决这个问题,我们可以将孙子进程管理请求生成的fd,由于子进程被wait,孙子进程会直接被系统领养,就会一直处理请求。
示例:TCPserver中loop接口中的实现方式
pid_t id = fork();
if (id == 0)
{
::close(_listensockfd);
pid_t rid = fork();
if (rid == 0)
{
Server(sockfd, peer);
}
exit(0);
}
waitpid(id, nullptr, 0);
::close(sockfd);
}
当然,这里我们也可以使用线程池或者是进程池的方式,提前申请创建一批线程或者进程。
2、上述接收数据部分存在的问题
TCP协议是面向连接的,这种协议在通信时较udp会比较安全,但是也存在一些缺点。例如,在tcp协议中,数据并不是向udp协议通信一样,一整个直接发过来,所以上述的代码是存在一定错误的。Tcp协议并不能保证一次性接收的信息就是整条信息,所以需要我们人工对其进行识别。
3、Tcp支持全双工的原因
在前面文件的部分种,我们了解到,其实文件通过系统调用向磁盘中写入是一个拷贝的过程,就是找出数据根据对应进程的文件描述符表中对应文件描述符,再由系统调用将数据拷贝到内核缓冲区中,由内核决定何时将缓冲区中的内容刷入对应的磁盘中。根据Linux系统一切皆文件的理念,我们可以得知当我们将数据交由系统调用后,数据被拷贝到发送缓冲区中,发送时机由内核(TCP协议)决定*(怎么发、错了怎么办都和上层没有关系)*,整个过程其实就是将数据从一台主句的发送缓冲区发送到另一台主机的接收缓冲区,相当是两个OS进行通信,对于接收方也是同理。
数据经过协议栈在不同主机进行相互拷贝,所以应用层的用户并不用细节,只需要将数据交由下层协议,让下层协议将数据拷贝到不同主机之间。
而我们的缓冲区中没有数据时,使用read、recv等接口就会阻塞,等待数据的发送过来。从另外一个角度看,其实缓冲区就像是我们前面学习的生产者消费者模型,只不过这里生产者变成了用户,消费者变成了OS。
当缓冲区被写满时,对应的系统调用接口会被阻塞,其本质就是用户层在进行同步,确保数据能够准确地发送给对端。
总结一下:其实从上面我们就可以看出,通信的本质就是拷贝。
通过上图我们看到,每个主机都两个缓冲区,这两个缓冲区互不干扰,这就是tcp支持全双工的原因,也是一个文件fd即支持写又支持读的原因。
以上就是所有内容,感谢阅读!!!