本节介绍 TCP 的套接字。 TCP通信有四种版本:
1.单进程版,没人用,不像UDP,TCP单进程只能和一个人通信! !
2.多进程版本2.0父子进程和2.1版本兄弟孙进程
3.普通多线程版本
4.单例模式线程池的多线程版本
代码上面的注释很重要应用编程接口和套接字,请仔细阅读! ! !
TCP socket(部分部分与UDP相同,不再介绍)
服务器需要以下功能
1.监听套接字用于与对端的套接字建立连接。服务器必须始终保持监听并等待随时与客户连接
2.accept socket应用编程接口和套接字,listen负责与peer建立连接,accept负责与peer通信,注意函数返回真正与peer通信的socket(文件描述符)
3.read,因为socket本身是一个文件描述符,所以可以使用read来接收信息,和UDP中引入的recvfrom函数是一样的
4.write,因为socket本身是一个文件描述符,所以可以使用write来发送信息,和UDP中引入的sendto函数是一样的
5.ip地址的主机顺序转换为网络字节顺序,反之亦然
客户端需要以下功能
1.与服务器连接套接字连接
白话总结TCP流程
Server:前两步同UDP
1.创建一个通信套接字,本质上是打开一个文件——只和系统有关
2.bind, struct sockaddr_in—> ip + port,本质是将ip + port与文件信息关联起来
3.listen建立连接,本质上是设置socket文件的状态,允许别人连接我
4.accept 启动通信服务,本质是获取到应用层的新连接,以fd为代表。因为操作系统会“先描述,组织”所有的连接,把所有的信息组织成结构
使用文件描述符来表示每个结构体,找到文件描述符,就可以找到对应的结构体信息。
所谓的“链接”,在OS层面,本质上是一个描述链接结构的“文件”
5.读/写的本质是网络通信。对于用户来说,相当于正常读写文件
6.connect 本质上是发起一个连接。在系统层面,就是建立一个请求消息,发送过去。在网络层面,发起 TCP 三向握手(握手和挥手后面会解释)
7.close,客户端&&服务器关闭文件,a。系统级,释放已申请的文件资源、连接资源等。 b.在网络层面,通知对方我的连接已经关闭!
本质在网络层面,挥手四次(后文解读)
四个反例理解TCP socket(四个反例的客户端和Makefile一样,主要区别是服务端)1.单进程版,没人用,不像UDP、TCP单进程只能一个人通信! !
生成文件
.PHONY:all
all: tcp_client tcp_server
tcp_client:tcp_client.cpp
g++ -o $@ $^ -std=c++11 -pthread
tcp_server:tcp_server.cpp
g++ -o $@ $^ -std=c++11 -pthread
.PHONY:clean
clean:
rm -rf tcp_client tcp_server
tcp_server.cpp
#include
using namespace std;
#include
#include
#include
#include
#include
#include
void Usage()
{
cout << "输入格式有误"
<< "应为:./tcp_server + 端口号" << endl;
}
int main(int argv, char *argc[])
{
if (argv != 2)
{
Usage();
return 1;
}
// 1.创建套接字
// SOCK_STREAM流式操作表示TCP
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
cerr << "sock create fail" << errno << endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); //为安全着想,将结构体变量初始化
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argc[1])); //此处的端口号,是我们计算机上的变量,是主机序列, 因为在栈上创建的,我们要转为网络字节序需要利用htons函数前面有介绍
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind fail" << errno << endl;
return 3;
}
//前面除了SOCK_STREAM都和UDP相同,现在开始有区别了
// 3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
// 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
// 我们当前写的是一个server, 周而复始的不间断的等待客户到来
// 我们要不断的给用户提供一个建立连接的功能
//
// 设置套接字是Listen状态, 本质是允许用户连接
const int back_log = 5;
if (listen(listen_sock, back_log) < 0)
{
cerr << "listen fail" << errno << endl;
return 3;
}
//version 1 : 现在做的是单进程版,没人使用,与UDP不同,TCP单进程只能与一个人进行通信!!
for (;;)
{
//我们需要提供一段空间来接收客户端的信息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//注意返回值也是个文件描述符,listen_sock负责用来等待客户端连接,newsock是真正与客户端进行IO通信的套接字,具体看函数介绍!
int newsock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if (newsock < 0)
{
continue;
}
cout<<"与客户端连接成功!"<<endl;
//链接成功开始服务
while (true)
{
//应为套接字也是文件描述符,所以当然可以用read与write来进行通信,效果与UDP中介绍的recvfrom与sendto相同
char buffer[1024];
memset(&buffer, 0, sizeof(buffer));
ssize_t s = read(newsock, buffer, sizeof(buffer));
if (s > 0) //说明接收到了信息
{
buffer[s] = 0; //将获取的内容当成字符串,尾部加0
cout << "成功接收到客户端信息:" << buffer << endl;
string message = "老哥我收到了!";
write(newsock, message.c_str(), message.size());
}
else if (s == 0) //说明客户端推出了链接
{
cout << " 客户端已退出 " << endl;
break;
}
else
{
cout << "read errno..." << endl;
break;
}
}
}
return 0;
}
tcp_client.cpp
#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
void Usage()
{
cout << "输入格式有误"
<< "应为:./tcp_client + ip + 端口号" << endl;
}
int main(int argv, char *argc[])
{
if (argv != 3)
{
Usage();
return 1;
}
// 1. 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "sock create fail" << errno << endl;
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons((uint16_t)atoi(argc[2]));//server_port
// inet_addr 该函数做两件事情
// 1. 将点分十进制的字符串风格的IP,转化成为4字节IP
// 2. 将4字节由主机序列转化成为网络序列
server.sin_addr.s_addr = inet_addr(argc[1]);//server_ip
//与UDP一样客户端不需要显示bind,OS会自动帮我们安全的绑定
// TCP的客户端需要用connect函数与服务端进行连接
// 2. 发起链接
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
cerr << "connect fail" << errno << endl;
return 3;
}
cout << "connect success!" << endl;
while (true)
{
cout << "请输入内容:";
char buffer[1024];
memset(&buffer, 0, sizeof(buffer));
fgets(buffer, sizeof(buffer) - 1, stdin);
write(sock, buffer, sizeof(buffer));
char recvbuf[1024];
memset(&recvbuf, 0, sizeof(recvbuf));
ssize_t s = read(sock, recvbuf, sizeof(recvbuf));
if (s > 0)
{
recvbuf[s] = 0;
cout << "收到服务端返回信息:" << recvbuf << endl;
}
}
return 0;
}
对于下面三个反例的子进程或线程,使用后记得关闭socket,否则会被占用,并且socket数量有限。 2.多进程版本2.0兄弟进程和2.1版本兄弟进程
tcp_server.cpp
#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
#include
void Usage()
{
cout << "输入格式有误"
<< "应为:./tcp_server + 端口号" << endl;
}
void ServiceIO(int newsock)
{
//链接成功开始服务
while (true)
{
//应为套接字也是文件描述符,所以当然可以用read与write来进行通信,效果与UDP中介绍的recvfrom与sendto相同
char buffer[1024];
memset(&buffer, 0, sizeof(buffer));
ssize_t s = read(newsock, buffer, sizeof(buffer));
if (s > 0) //说明接收到了信息
{
buffer[s] = 0; //将获取的内容当成字符串,尾部加0
cout << "成功接收到客户端信息:" << buffer << endl;
string message = "老哥我收到了!";
write(newsock, message.c_str(), message.size());
}
else if (s == 0) //说明客户端推出了链接
{
cout << " 客户端已退出 " << endl;
break;
}
else
{
cout << "read errno..." << endl;
break;
}
}
}
int main(int argv, char *argc[])
{
if (argv != 2)
{
Usage();
return 1;
}
// 1.创建套接字
// SOCK_STREAM流式操作表示TCP
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
cerr << "sock create fail" << errno << endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); //为安全着想,将结构体变量初始化
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argc[1])); //此处的端口号,是我们计算机上的变量,是主机序列, 因为在栈上创建的,我们要转为网络字节序需要利用htons函数前面有介绍
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind fail" << errno << endl;
return 3;
}
//前面除了SOCK_STREAM都和UDP相同,现在开始有区别了
// 3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
// 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
// 我们当前写的是一个server, 周而复始的不间断的等待客户到来
// 我们要不断的给用户提供一个建立连接的功能
//
// 设置套接字是Listen状态, 本质是允许用户连接
const int back_log = 5;
if (listen(listen_sock, back_log) < 0)
{
cerr << "listen fail" << errno << endl;
return 3;
}
//多进程2.0版本 2.1版本不需要写,因为资源会自动被OS释放掉
signal(SIGCHLD, SIG_IGN); //在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
for (;;)
{
//我们需要提供一段空间来接收客户端的信息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//注意返回值也是个文件描述符,listen_sock负责用来等待客户端连接,newsock是真正与客户端进行IO通信的套接字,具体看函数介绍!
int newsock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if (newsock < 0)
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port); //将网络序转为主机序
string cli_ip = inet_ntoa(peer.sin_addr); //将网络序转主机序,再将主机序的4字节转为字符串序列
cout << "与客户端连接成功,套接字为:" << newsock << " ip :" << cli_ip << " port :" << cli_port << endl;
pid_t id = fork();
if (id < 0)
{
cout << "创建子进程失败" << endl;
continue;
}
//多进程2.0版本
else if (id == 0) //曾经被父进程打开的fd,是否会被子进程继承呢? 无论父子进程中的哪一个,强烈建议关闭掉不需要的fd
{
// child
close(listen_sock);
ServiceIO(newsock);
//子进程使用完要把文件描述符关闭了,不然文件描述符数量是有上限的
//如果不关闭不需要的文件描述符,会造成文件描述符泄露
exit(0);
}
else
{
//正常父进程要阻塞等待子进程wait_pid(),为了提高效率,利用信号学的知识signal,忽略掉子进程,让子进程结束了自己退出释放资源。
// father
// do nothing
close(newsock);
}
//多进程2.1版本
// else if (id == 0) //曾经被父进程打开的fd,是否会被子进程继承呢? 无论父子进程中的哪一个,强烈建议关闭掉不需要的fd
// {
// close(listen_sock);
// if (fork() > 0)
// exit(0); //退出的是子进程
// //向后走的是孙子进程, 因为孙子的父亲进程已经退出了,孙子成为孤儿进程,会由操作系统对他进行资源释放
// ServiceIO(newsock);
// exit(0);
// }
// else
// {
// //正常父进程要阻塞等待子进程wait_pid(),为了提高效率,利用信号学的知识signal,忽略掉子进程,让子进程结束了自己退出释放资源。
// // father
// // do nothing
// //2.1版本需要接收子进程,但不会造成堵塞,因为子进程刚创建就结束了,主要是孙子进程在工作,与父进程无关!
// waitpid(id, nullptr, 0);
// close(newsock);
// }
}
return 0;
}
运行结果如下:因为子2.0进程或者2.1子进程在完成任务后关闭了文件描述符4,所以重连还是会使用4,不会占用多个。
3.普通多线程版本
tcp_server.cpp
#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
#include
#include
void Usage()
{
cout << "输入格式有误"
<< "应为:./tcp_server + 端口号" << endl;
}
void ServiceIO(int newsock)
{
//链接成功开始服务
while (true)
{
//应为套接字也是文件描述符,所以当然可以用read与write来进行通信,效果与UDP中介绍的recvfrom与sendto相同
char buffer[1024];
memset(&buffer, 0, sizeof(buffer));
ssize_t s = read(newsock, buffer, sizeof(buffer));
if (s > 0) //说明接收到了信息
{
buffer[s] = 0; //将获取的内容当成字符串,尾部加0
cout << "成功接收到客户端信息:" << buffer << endl;
string message = "老哥我收到了!";
write(newsock, message.c_str(), message.size());
}
else if (s == 0) //说明客户端推出了链接
{
cout << " 客户端已退出 " << endl;
break;
}
else
{
cout << "read errno..." << endl;
break;
}
}
}
void *Handler(void * argc)
{
//线程分离,自动释放资源
pthread_detach(pthread_self());
int sock = *(int *)argc;
ServiceIO(sock);
close(sock);
}
int main(int argv, char *argc[])
{
if (argv != 2)
{
Usage();
return 1;
}
// 1.创建套接字
// SOCK_STREAM流式操作表示TCP
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
cerr << "sock create fail" << errno << endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); //为安全着想,将结构体变量初始化
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argc[1])); //此处的端口号,是我们计算机上的变量,是主机序列, 因为在栈上创建的,我们要转为网络字节序需要利用htons函数前面有介绍
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind fail" << errno << endl;
return 3;
}
//前面除了SOCK_STREAM都和UDP相同,现在开始有区别了
// 3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
// 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
// 我们当前写的是一个server, 周而复始的不间断的等待客户到来
// 我们要不断的给用户提供一个建立连接的功能
//
// 设置套接字是Listen状态, 本质是允许用户连接
const int back_log = 5;
if (listen(listen_sock, back_log) < 0)
{
cerr << "listen fail" << errno << endl;
return 3;
}
//多线程版
for (;;)
{
//我们需要提供一段空间来接收客户端的信息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//注意返回值也是个文件描述符,listen_sock负责用来等待客户端连接,newsock是真正与客户端进行IO通信的套接字,具体看函数介绍!
int newsock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if (newsock < 0)
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port); //将网络序转为主机序
string cli_ip = inet_ntoa(peer.sin_addr); //将网络序转主机序,再将主机序的4字节转为字符串序列
cout << "与客户端连接成功,套接字为:" << newsock << " ip :" << cli_ip << " port :" << cli_port << endl;
//曾经被主线程打开的fd,新线程是共享的,因此不能关闭文件描述符,只要在线程执行函数中关闭就好了
pthread_t tid;
pthread_create(&tid, nullptr, Handler, (void *)(&newsock));
}
return 0;
}
运行结果如下:因为文件描述符在线程完成任务后关闭,所以多个连接不会占用多个描述符。通常只需要打开 4 个,因为 4 个用完然后关闭。回来,显示5的问题可能是复制问题。
4.单例模式线程池的多线程版本
tcp_server.cpp
#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"task.hpp"
#include"thread_pool.hpp"
using namespace ns_task;
using namespace ns_thread;
void Usage()
{
cout << "输入格式有误"
<< "应为:./tcp_server + 端口号" << endl;
}
int main(int argv, char *argc[])
{
if (argv != 2)
{
Usage();
return 1;
}
// 1.创建套接字
// SOCK_STREAM流式操作表示TCP
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
cerr << "sock create fail" << errno << endl;
return 2;
}
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); //为安全着想,将结构体变量初始化
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argc[1])); //此处的端口号,是我们计算机上的变量,是主机序列, 因为在栈上创建的,我们要转为网络字节序需要利用htons函数前面有介绍
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind fail" << errno << endl;
return 3;
}
//前面除了SOCK_STREAM都和UDP相同,现在开始有区别了
// 3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
// 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
// 我们当前写的是一个server, 周而复始的不间断的等待客户到来
// 我们要不断的给用户提供一个建立连接的功能
//
// 设置套接字是Listen状态, 本质是允许用户连接
const int back_log = 5;
if (listen(listen_sock, back_log) < 0)
{
cerr << "listen fail" << errno << endl;
return 3;
}
//线程池版
for (;;)
{
//我们需要提供一段空间来接收客户端的信息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//注意返回值也是个文件描述符,listen_sock负责用来等待客户端连接,newsock是真正与客户端进行IO通信的套接字,具体看函数介绍!
int newsock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if (newsock < 0)
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port); //将网络序转为主机序
string cli_ip = inet_ntoa(peer.sin_addr); //将网络序转主机序,再将主机序的4字节转为字符串序列
cout << "与客户端连接成功,套接字为:" << newsock << " ip :" << cli_ip << " port :" << cli_port << endl;
Task t(newsock);
ThreadPool<Task>::GetInstance()->Push(t);
}
return 0;
}
thread_pool.hpp
#pragma once
#include
using namespace std;
#include
#include
#include
namespace ns_thread
{
const int default_num = 5;
pthread_mutex_t mtx;
pthread_cond_t c_cnd;
template <class T>
class ThreadPool
{
private:
//线程池中线程个数
int _num;
queue<T> task_queue;
//静态成员变量要在类外初始化!!
static ThreadPool<T> *ins;
//单例模式,构造函数必须得实现,但是必须的私有化
ThreadPool(int num = default_num)
: _num(num)
{
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&c_cnd, nullptr);
}
//拷贝构造
ThreadPool(const ThreadPool<T> &tp) = delete;
ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;
public:
void Lock()
{
pthread_mutex_lock(&mtx);
}
void Unlock()
{
pthread_mutex_unlock(&mtx);
}
void Wait()
{
pthread_cond_wait(&c_cnd, &mtx);
}
void WakeUp()
{
pthread_cond_signal(&c_cnd);
}
bool IsEmpty()
{
return task_queue.empty();
}
public:
static ThreadPool<T> *GetInstance()
{
static pthread_mutex_t _lock = PTHREAD_MUTEX_INITIALIZER;
//双判断,减少锁的征用,提高获取单例的效率
if (ins == nullptr)
{
pthread_mutex_lock(&_lock);
if (ins == nullptr)
{
//初始化
ins = new ThreadPool<T>();
ins->InitThreadPool();
cout << "首次加载对象" << endl;
}
pthread_mutex_unlock(&_lock);
}
return ins;
}
// 在类中要让线程执行类内成员方法,是不可行的!!因为会有隐藏this指针就不符合创建线程的格式了
// 必须让线程执行静态方法,静态成员函数无法访问私有属性
static void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
//挂起等待
tp->Wait();
}
//处理任务
T t;
tp->Pop(&t);
tp->Unlock();
//这里先释放锁再进行任务处理,能达到一个线程处理任务,另一个线程可以抢锁获取数据,达到了并行的效果。
//如果先处理任务再释放锁,那么每个线程处理任务就是串行的,效率肯定没有并行的高
t.Run();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < _num; i++)
{
pthread_create(&tid, nullptr, Rountine, (void *)this);
}
}
void Push(const T &in)
{
Lock();
task_queue.push(in);
Unlock();
//唤醒线程处理任务
WakeUp();
}
void Pop(T *out)
{
*out = task_queue.front();
task_queue.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&c_cnd);
}
};
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
task.hpp
#pragma once
#include
using namespace std;
#include
namespace ns_task
{
class Task
{
private:
int _sock;
public:
Task(){};
Task(int sock)
: _sock(sock)
{
}
void Run()
{
//链接成功开始服务
while (true)
{
//应为套接字也是文件描述符,所以当然可以用read与write来进行通信,效果与UDP中介绍的recvfrom与sendto相同
char buffer[1024];
memset(&buffer, 0, sizeof(buffer));
ssize_t s = read(_sock, buffer, sizeof(buffer));
if (s > 0) //说明接收到了信息
{
buffer[s] = 0; //将获取的内容当成字符串,尾部加0
cout << "成功接收到客户端信息:" << buffer << endl;
string message = "老哥我收到了!";
write(_sock, message.c_str(), message.size());
}
else if (s == 0) //说明客户端推出了链接
{
cout << " 客户端已退出 " << endl;
break;
}
else
{
cout << "read errno..." << endl;
break;
}
}
close(_sock);
}
~Task(){};
};
}
线程池的作用如下:第一次建立连接时,建立多个线程。