网路聊天
API详解
下面用到的API,都在sys/socket.h中
socket ():
- socket() 打开一个网络通讯端口,如果成功的话,就像open() 一样返回一个文件描述符
- 应用程序可以像读文件一样用read/write在网络上收发数据
- 如果调用出错返回-1
- 对于IPv4,family参数指定为AF_INET
- 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议
- protocol参数是传输层协议,填0默认匹配
bind ()
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后,就可以向服务器发起连接,服务器需要调用bind绑定一个固定的网络地址和端口号
- bind() 成功返回0,失败返回-1
- 作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听nyaddr所描述的地址和端口号
- struct sockaddr* 是一个通用指针类型,myaddr参数实际上可以接收各种协议 的sockaddr结构体,而他们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
myaddr初始化:
1.整个结构体清零
2.设置地址类型为AF_INET
3.网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个ip地址
4.端口号为SERV_PORT,可以定义为9999
listen ()
- listen() 声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置不会太大(一般是5)
- listen() 成功返回0,失败返回-1
accept ()
- 三次握手完成后,服务器调用accept接收连接
- 如果服务器调用accept时还没有客户端的 连接请求,就阻塞等待直到有客户端连接上来
- addr是一个传出参数,accept返回时传出客户端的地址和端口号
- 如果给addr传递NULL,表示不关心客户端的地址
- addrlen参数是一个传入传出参数(value-result arument),传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地质结构体的实际长度(有可能没有占满调用者提供的缓冲区)
connect ()
- 客户端需要调用connect连接服务器
- connct和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址
- 成功返回0,出错返回-1
服务端
成员变量三个,分别是listen套接字,ip和端口号,服务端的ip和端口号必须固定
构造函数传入ip和地址初始化成员变量
init函数初始化套接字信息,绑定到设备上,和前面的udp一样
因为tcp是面向字节流的,一般比较被动,处于一种等待连接来的状态,所以要设置等待连接,listen状态:
run函数开始接收消息,创建和套接字的连接,返回新的套接字描述符,用来后面通讯
为什么会有两个套接字
这个就像饭店拉客的例子,饭店门口有一个吆喝顾客进去吃饭的服务员,当接受这个建议后,进到店里,会重新分配一个服务员提供后续服务,点餐等,吆喝的服务员又会继续在门口等待下一个顾客
listen套接字是拉客的服务员,确保服务端需要通信,后续的发送和接收由accept创建的新套接字完成
service函数发送和接收消息,传入套接字,ip和port用来显示用户。tcp字节流读写可以直接用read和write函数
测试
开启服务端,服务端使用telnet命令,使用本机环回地址连接
telnet [ip] [端口]
接着输入^]就可以输入消息
全
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "log.hpp"
string defaultip = "0.0.0.0";
uint16_t defaultport = 8000;
const int back = 2; //连接请求队列个数,一般2-4
Log log;
enum
{
SOCK_ERR = 1,
BIND_ERR,
LIStEN_ERR
};
class server
{
public:
server(const string ip = defaultip, const uint16_t port = defaultport)
{
_ip = defaultip;
_port = defaultport;
}
void init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
log.logmessage(fatal, "socket create error");
exit(SOCK_ERR);
}
log.logmessage(info, "socket create success:%d", _listensock);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
inet_aton(_ip.c_str(), &local.sin_addr);
local.sin_port = htons(_port);
//绑定
if (bind(_listensock, (const struct sockaddr*)&local, sizeof(local)) < 0)
{
log.logmessage(fatal, "bind error");
exit(BIND_ERR);
}
log.logmessage(fatal, "bind success");
//tcp面向连接,服务器一般被动,处于一种等待连接到来的状态
// 设置listen状态
if (listen(_listensock, back) < 0)
{
log.logmessage(fatal, "listen error");
exit(LIStEN_ERR);
}
}
void service(int fd, const string ip, const uint16_t port)
{
cout << "welcome " << ip << endl;
char message[1024];
while (true)
{
ssize_t n = read(fd, message, sizeof(message) - 1);
if (n > 0)
{
message[n] = 0;
//数据处理
string echo_string;
echo_string = "[" + ip + ":" + to_string(port) + "]: " + message;
cout << echo_string << endl;
write(fd, echo_string.c_str(), echo_string.size());
}
}
}
void run()
{
while (true)
{
//获取链接
sockaddr_in sock;
socklen_t len;
int sockfd = accept(_listensock, (sockaddr *)&sock, &len);
if (sockfd < 0)
{
log.logmessage(warning, "accept fail");
continue;
}
char buff[32];
string clientip = inet_ntop(AF_INET, &sock.sin_addr, buff, len);
uint16_t clientport = ntohs(sock.sin_port);
service(sockfd, clientip, clientport);
close(sockfd);
}
}
~server()
{
close(_listensock);
}
private:
int _listensock;
string _ip;
uint16_t _port;
};
客户端
初始化addr协议内容
套接字
conncect函数建立连接
读写消息
全
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <fcntl.h>
using namespace std;
// string path = "/dev/pts/5";
// struct thread_data
// {
// int _sockfd;
// struct sockaddr_in _server;
// string _ip;
// };
// void *send_message(void * temp)
// {
// thread_data *td = (struct thread_data*)temp;
// string message;
// cout << "welcome " << td->_ip << endl;
// while (true)
// {
// cout << "please enter: ";
// getline(cin, message);
// sendto(td->_sockfd, message.c_str(), message.size(), 0, (const sockaddr *)&td->_server, sizeof(td->_server));
// }
// }
// void* recv_message(void* temp)
// {
// // int fd = open(path.c_str(), O_WRONLY);
// // if (fd < 0)
// // {
// // perror("open");
// // }
// // dup2(fd, 2);
// thread_data *td = (struct thread_data *)temp;
// char buff[1024];
// sockaddr_in rec;
// socklen_t len = sizeof(rec);
// while (true)
// {
// ssize_t s = recvfrom(td->_sockfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&rec, &len);
// if (s > 0)
// {
// buff[s] = 0;
// cerr << buff << endl;
// }
// }
// //close(fd);
// }
int main()
{
//thread_data td;
//ip和port可以通过命令行参数传入
uint16_t port = 8000;
string ip = "ip";
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cout << "socket error" << endl;
}
//建立连接
if (connect(sockfd, (const struct sockaddr*)&server, sizeof(server)) < 0)
{
cout << "connect error" << endl;
}
cout << "connect success" << endl;
// 通信
string message;
char buff[1024];
while (true)
{
cout << "please enter:";
getline(cin, message);
write(sockfd, message.c_str(), message.size());
ssize_t n = read(sockfd, buff, sizeof(buff) - 1);
if (n > 0)
{
buff[n] = 0;
cout << buff << endl;
}
}
// client 需要绑定,但不显示绑定,由os自由选择
// pthread_t tid_send;
// pthread_t tid_recv;
// pthread_create(&tid_send, nullptr, send_message, &td);
// pthread_create(&tid_recv, nullptr, recv_message, &td);
// pthread_join(tid_send, nullptr);
// pthread_join(tid_recv, nullptr);
close(sockfd);
}
多进程版
上面的程序是单进程的,主进程会跟随sevice程序直到结束,只能提供一个客户链接。需要可以多个用户同时连接的
在service函数的时候fork子进程,让子进程去执行功能,双方关闭不需要的文件,子进程关闭listen套接字,父进程关闭sockfd套接字,互不影响。但父进程会卡在回收子进程的wait函数里,可以用非阻塞的等待,保存打开过的套接字轮询。还可以在fork的子进程里再一次fork,fork成功后子进程退出,这样代替执行功能的就是孙子进程,孙子进程会被os领养,结束后自动释放,同时设置进程退出信号为忽视,就不需要等待子进程
void run()
{
//忽视子进程
signal(SIGCHLD, SIG_IGN);
while (true)
{
//获取链接
sockaddr_in sock;
socklen_t len;
int sockfd = accept(_listensock, (sockaddr *)&sock, &len);
if (sockfd < 0)
{
log.logmessage(warning, "accept fail");
continue;
}
char buff[32];
string clientip = inet_ntop(AF_INET, &sock.sin_addr, buff, len);
uint16_t clientport = ntohs(sock.sin_port);
pid_t pid = fork();
if (pid == 0)
{
// 子进程,孙子进程执行,子进程直接回收
if (fork() > 0)
exit(0); // 孙子进程,system领养
service(sockfd, clientip, clientport);
close(_listensock);
exit(0);
}
// 父进程
pid_t ret = waitpid(pid, nullptr, 0);
close(sockfd);
}
}
可以从函数开始就for循环fork,每一个子进程都会从listen套接字中获取标识符,执行service。只不过这种竞争性的需要给accept加锁
多线程版
创建进程的成本比较大,所以可以适用多线程版本
线程调用的函数来提供service功能,需要传入文件符,ip和port,所以创建一个结构体作为参数。线程执行的routine函数在类内需是静态的,主线程等待线程退出也会阻塞,所以将线程分类,退出信号忽视。静态函数无法调用非静态函数,所以传入的参数内带一个server类变量,向前声明server类,用this指针初始化,用来调用service函数
struct thread_data
{
int _fd;
string _ip;
uint16_t _port;
server* ser;
};
static void* routine(void* tmp)
{
//线程分离
pthread_detach(pthread_self());
//可以service函数提到类外
//添加一个类成员
thread_data *td = static_cast<thread_data*>(tmp);
td->ser->service(td->_fd, td->_ip, td->_port);
delete td;
return nullptr;
}
pthread_t tid;
thread_data* td = new thread_data;
td->_fd = sockfd;
td->_ip = clientip;
td->_port = clientport;
td->ser = this;
pthread_create(&tid, nullptr, routine, td);
线程池版
多线程版本,如果有一个客户就要建立一个线程,当高峰的时候,可能会有很多个线程同时开启,资源占用大
加入之前写好的线程池,当接收到一个会话后,派给线程池,线程池会自动分配线程执行任务
修改tash任务类,成员保存文件符,ip和port
读取内容处理后写回去,关闭文件。常服务不在线程池里跑,执行一次后结束会话
全
ThreadPool
#pragma once
#include <vector>
#include <queue>
#include <pthread.h>
#include <string>
#include <unistd.h>
//换为封装的线程
struct ThreadInfo
{
pthread_t _tid;
std::string _name;
};
template <class T>
class pool
{
static const int defaultnum = 5;
public:
std::string getname(pthread_t tid)
{
for (auto ch : _thread)
{
if (ch._tid == tid)
{
return ch._name;
}
}
return "None";
}
static void* HandlerTask(void* args)
{
pool<T> *tp = static_cast<pool<T> *>(args);
std::string name = tp->getname(pthread_self());
while (true)
{
pthread_mutex_lock(&(tp->_mutex));
while (tp->_que.empty())
{
pthread_cond_wait(&(tp->_cond), &(tp->_mutex));
}
T t = tp->_que.front();
tp->_que.pop();
pthread_mutex_unlock(&tp->_mutex);
t.run();
//printf("%s finsih task:%s\n", name.c_str(), t.getresult().c_str());
//sleep(1);
}
}
void start()
{
for (int i = 0; i < _thread.size(); i++)
{
_thread[i]._name = "thread" + std::to_string(i);
pthread_create(&_thread[i]._tid, nullptr, HandlerTask, this);
}
}
void push(const T& x)
{
pthread_mutex_lock(&_mutex);
_que.push(x);
pthread_cond_signal(&_cond);
pthread_mutex_unlock(&_mutex);
}
static pool<T>* GetInstance()
{
//套一层判断,只有第一次需要上锁
if (_pl == nullptr)
{
pthread_mutex_lock(&_lock);
if (_pl == nullptr)
{
//printf("first create\n");
_pl = new pool<T>;
}
pthread_mutex_unlock(&_lock);
}
return _pl;
}
private:
//构造私有化
pool(int num = defaultnum)
: _thread(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
pool(const pool<T> &) = delete;
const pool<T> &operator=(const pool<T>&) = delete;
~pool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
std::vector<ThreadInfo> _thread;
std::queue<T> _que;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static pthread_mutex_t _lock;
static pool<T> *_pl;
};
//类外初始化
template <class T>
pool<T>* pool<T>::_pl = nullptr;
template <class T>
pthread_mutex_t pool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
task
#pragma once
#include <stdio.h>
#include <string>
#include "log.hpp"
#include "init.hpp"
// enum
// {
// DIVZERO = 1,
// UNKNOW
// };
extern Log log;
Dict dict;
struct task
{
public:
task(int fd, string ip, uint16_t port)
{
_fd = fd;
_ip = ip;
_port = port;
}
void run()
{
//常服务在线程池跑不合理,循环
cout << "welcome " << _ip << endl;
char message[1024];
ssize_t n = read(_fd, message, sizeof(message) - 1);
if (n > 0)
{
message[n] = 0;
// 数据处理
string echo_string;
echo_string = "[" + _ip + ":" + to_string(_port) + "]: " + message;
cout << echo_string << endl;
write(_fd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
log.logmessage(info, "quit server");
}
else
{
log.logmessage(warning, "read error");
}
close(_fd);
}
private:
int _fd;
string _ip;
uint16_t _port;
};
server
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
//#include <netinet/in.h>
#include <wait.h>
#include <signal.h>
#include "ThreadPool.hpp"
#include "task.hpp"
#include "log.hpp"
string defaultip = "0.0.0.0";
uint16_t defaultport = 8000;
const int back = 2; //连接请求队列个数,一般2-4
Log log;
enum
{
SOCK_ERR = 1,
BIND_ERR,
LIStEN_ERR
};
class server;
struct thread_data
{
int _fd;
string _ip;
uint16_t _port;
server* ser;
};
class server
{
public:
server(const string ip = defaultip, const uint16_t port = defaultport)
{
_ip = defaultip;
_port = defaultport;
}
void init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
log.logmessage(fatal, "socket create error");
exit(SOCK_ERR);
}
log.logmessage(info, "socket create success:%d", _listensock);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
inet_aton(_ip.c_str(), &local.sin_addr);
local.sin_port = htons(_port);
//绑定
if (bind(_listensock, (const struct sockaddr*)&local, sizeof(local)) < 0)
{
log.logmessage(fatal, "bind error:%s",strerror(errno));
exit(BIND_ERR);
}
log.logmessage(info, "bind success");
//tcp面向连接,服务器一般被动,处于一种等待连接到来的状态
// 设置listen状态
if (listen(_listensock, back) < 0)
{
log.logmessage(fatal, "listen error");
exit(LIStEN_ERR);
}
}
// void service(int fd, const string ip, const uint16_t port)
// {
// cout << "welcome " << ip << endl;
// char message[1024];
// while (true)
// {
// ssize_t n = read(fd, message, sizeof(message) - 1);
// if (n > 0)
// {
// message[n] = 0;
// //数据处理
// string echo_string;
// echo_string = "[" + ip + ":" + to_string(port) + "]: " + message;
// cout << echo_string << endl;
// write(fd, echo_string.c_str(), echo_string.size());
// }
//读到0和其他情况
// }
// }
// static void* routine(void* tmp)
// {
// //线程分离
// pthread_detach(pthread_self());
// //可以service函数提到类外
// //添加一个类成员
// thread_data *td = static_cast<thread_data*>(tmp);
// td->ser->service(td->_fd, td->_ip, td->_port);
// delete td;
// return nullptr;
// }
void run()
{
pool<task>::GetInstance()->start();
// 忽视子进程
//signal(SIGCHLD, SIG_IGN);
while (true)
{
//获取链接
struct sockaddr_in sock;
socklen_t len = sizeof(sock);
int sockfd = accept(_listensock, (sockaddr *)&sock, &len);
if (sockfd < 0)
{
log.logmessage(warning, "accept fail");
continue;
}
char buff[32];
string clientip = inet_ntop(AF_INET, &sock.sin_addr, buff, len);
uint16_t clientport = ntohs(sock.sin_port);
log.logmessage(info, "get a new link,sockfd:%d", sockfd);
// 线程池
task t(sockfd, clientip, clientport);
pool<task>::GetInstance()->push(t);
//多线程
// pthread_t tid;
// thread_data* td = new thread_data;
// td->_fd = sockfd;
// td->_ip = clientip;
// td->_port = clientport;
// td->ser = this;
// pthread_create(&tid, nullptr, routine, td);
// 多进程
// pid_t pid = fork();
// if (pid == 0)
// {
// // 子进程,孙子进程执行,子进程直接回收
// close(_listensock);
// if (fork() > 0)
// exit(0); // 孙子进程,system领养
// service(sockfd, clientip, clientport);
// close(sockfd);
// exit(0);
// }
// // 父进程
// pid_t ret = waitpid(pid, nullptr, 0);
// close(sockfd);
}
}
~server()
{
close(_listensock);
}
private:
int _listensock;
string _ip;
uint16_t _port;
};
英汉互译
准备一个词典文件,用:分割单词和汉语
init类加载词典和提供翻译功能
类成员用unorderedmap,string格式的单词查找存储的string类型汉语值
初始化功能打开文件,分割单词和翻译,加载到数据结构内
翻译功能遍历map查找翻译
task文件开始时初始化词典,收到数据调用翻译功能,将结果写会给客户
Dict dict;
echo_string = dict.translate(message);
init.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
using namespace std;
const string filename = "./dict.txt";
static void split(string line, string* key, string* value)
{
auto s = line.find(":");
if (s != string::npos)
{
*key = line.substr(0, s);
*value = line.substr(s + 1);
}
}
class Dict
{
public:
Dict()
{
cout << "开始打开" << endl;
ifstream dic(filename);
if (!dic.is_open())
{
cout << "file open error" << endl;
exit(1);
}
string line;
while (getline(dic, line))
{
string patr1;
string part2;
split(line, &patr1, &part2);
_dict.insert({patr1, part2});
}
dic.close();
}
string translate(string word)
{
for (auto ch : _dict)
{
if (ch.first == word)
{
return ch.second;
}
}
return "";
}
~Dict()
{
}
private:
unordered_map<string, string> _dict;
};
错误处理
当客户端发送请求,服务端正在往回写时,客户端的文件已经关闭,往一个不存在的文件写入内容就会发生错误,为了避免这种情况,刚开始的时候先关闭SIGPIP信号
write函数也有返回值,返回成功写入的字符个数,可以通过返回值判断写入情况
掉线重连
如果客户端在网络不好等其他情况掉线了,首先需要知道出错,也就是读或者写出错误,需要重新连接,模仿一个重连的情况
重连设置一定次数和时间间隔,用bool变量控制是否需要重连,读取成功后关闭文件重连
客户端发送数据的时候,服务端关闭,这时候客户端掉线重连了两次,重启服务端后连接成功
上面的情况有时候可能因为一个端口绑定后无法立即再次绑定,需要调用setsockopt函数复用之前的端口和ip启动