昨天写了一篇实现UDP通信的博客,UDP和TCP当然是两个极端,今天我们也来介绍一下TCP,在上一篇博客中,博主也介绍了一些TCP常用的系统调用接口,在这里博客就不再赘述了。另外,在这里祝福大家以及我参加秋招和找实习生的伙伴,拿个好offer,我们千万不要等毕业再去找工作,那时候失去应届生身份后就特别难进大厂了。
目录
认识TCP协议
单进程版本
Makefile
tcp_client.cc
tcp_server.cc
多进程版本
Makefile
tcp_client.cc
tcp_server.cc
多进程版本2
tcp_server.cc
单线程版本
tcp_server.cc
线程池版本
Makefile
task.hpp
tcp_client.cc
tcp_server.cc
thread_pool.hpp
测试结果(只展线程池版本的测试结果)
总结
服务端
客户端
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
1、传输层协议2、有连接
3、可靠传输4、面向字节流
关于TCP协议更多的知识讲解,我们到传输层报文那里着重介绍。
.PHONY:all
all:tcp_client tcp_server
udp_client:tcp_client.cc
g++ -o $@ $^ -std=c++11
udp_server:tcp_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f tcp_client tcp_server
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}
//./tcp_client server_ip server_port
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
//从命令行上获取的
string svr_ip = argv[1];
uint16_t port = atoi(argv[2]);
//1、创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cerr << "socket error!" << endl;
return 2;
}
//客户端不需要bind!
//2、发起链接
struct sockaddr_in server;
bzero(&server, sizeof(server)); //清0
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(svr_ip.c_str()); //客户端需要绑定明确的ip
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
cout << "connect server failed!" << endl;
return 3;
}
cout << "connect success!" << endl;
//3、进行正常的业务请求了
while (true)
{
cout << "Plase Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
write(sock, buffer, strlen(buffer)); //不要用sizeof()buffer, 因为不一定是要写1024字节
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
cout << "Server echo# " << buffer << endl;
}
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << "port" << endl;
}
void ServiceIO(int new_sock)
{
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
cout << "client# " << buffer << endl;
string echo_hello = ">>>server<<<: ";
echo_hello += buffer;
write(new_sock, echo_hello.c_str(),echo_hello.size());
}
else if(s == 0)
{
cout << "client quit..." << endl;
}
else
{
cerr << "read error" << endl;
break;
}
}
}
// ./tcp_server port
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1、创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0)
{
cerr << "socket error: " << errno << endl;
return 2;
}
//2、bind 绑定
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY; //可以接收任意ip
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind cerror: " << errno << endl;
return 3;
}
//3、套接字设置成监听状态
const int back_log = 5;
if(listen(listen_sock, back_log) < 0)
{
cerr << "listen cerror: " << errno << endl;
return 4;
}
signal(SIGCHLD, SIG_IGN); //子进程退出后会自动释放资源
while(1)
{
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
int new_sock = accept(listen_sock, (struct sockaddr*)&perr, &len);
if(new_sock < 0)
{
continue; //建立连接失败,不是直接终止,还需要继续
}
cout << "get a new link ..." << new_sock << endl;
ServiceIO(new_sock); //拿到链接直接干活
}
return 0;
}
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.cc
g++ -o $@ $^ -std=c++11
tcp_server:tcp_server.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcp_client tcp_server
客户端和上面单进程版本一样,这里以及下面只改变服务端的代码。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << " prot" << endl;
}
void ServiceIO(int new_sock)
{
//提供服务,我们是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));//空间初始化为0
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;//将获取的内容当成字符串
cout << "client# " << buffer << endl;
string echo_hello = ">>>server<<<: ";
echo_hello += buffer;
write(new_sock, echo_hello.c_str(), echo_hello.size());
}
else if(s == 0)
{
cout << "client quit ..." << endl;
break;
}
else
{
cerr << "read error" << endl;
break;
}
}
}
// .tcp_server 8081
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//tcp server
//1、创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0)
{
cerr << "socket error: " << errno << endl;
return 2;
}
//2、bind 绑定
struct sockaddr_in local;
local.sin_family = AF_INET; //协议种类
local.sin_port = htons(atoi(argv[1])); //端口号
local.sin_addr.s_addr = INADDR_ANY; //ip
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error: " << errno << endl;
return 3;
}
//3、因为tcp是面向连接的,a. 在通信前,需要建立连接 b. 然后才能通信
//一定有人主动建立(客户端,需要服务的一方建立连接),
//一定有人被动接收连接(服务端,童工服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//设置套接字是listen状态,本质是允许用户连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0)
{
cerr << "listen error: " << errno << endl;
return 4;
}
signal(SIGCHLD, SIG_IGN);//子进程退出后会自动释放资源,Linux下才能使用
for(; ;)
{
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
int new_sock = accept(listen_sock, (struct sockaddr*)&perr, &len);
if(new_sock < 0)
{
continue;
}
cout << "get a new link ...-> " << new_sock << endl;
pid_t id = fork();
if(id < 0)
{
continue;
}
else if(id == 0) //曾经被父进程打开的fd,会被子进程继承
{
//child
close(listen_sock); //关闭掉继承父进程下来的listen_sock,子进程不需要listen_sock
ServiceIO(new_sock);
close(new_sock); //子进程退出前关闭套接字
exit(0);
}
else
{
//父进程不要关闭listen_sock
close(new_sock);//父进程不需要,new_sock是给子进程使用
//不能直接wait,否则父进程可能会一直等子进程,这样就有变成了单进程版本了
}
//version 1: 单进程版,没人使用
//ServiceIO(new_sock);
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << " prot" << endl;
}
void ServiceIO(int new_sock)
{
//提供服务,我们是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));//空间初始化为0
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;//将获取的内容当成字符串
cout << "client# " << buffer << endl;
string echo_hello = ">>>server<<<: ";
echo_hello += buffer;
write(new_sock, echo_hello.c_str(), echo_hello.size());
}
else if(s == 0)
{
cout << "client quit ..." << endl;
break;
}
else
{
cerr << "read error" << endl;
break;
}
}
}
// .tcp_server 8081
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//tcp server
//1、创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0)
{
cerr << "socket error: " << errno << endl;
return 2;
}
//2、bind 绑定
struct sockaddr_in local;
local.sin_family = AF_INET; //协议种类
local.sin_port = htons(atoi(argv[1])); //端口号
local.sin_addr.s_addr = INADDR_ANY; //ip
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error: " << errno << endl;
return 3;
}
//3、因为tcp是面向连接的,a. 在通信前,需要建立连接 b. 然后才能通信
//一定有人主动建立(客户端,需要服务的一方建立连接),
//一定有人被动接收连接(服务端,童工服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//设置套接字是listen状态,本质是允许用户连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0)
{
cerr << "bind error: " << errno << endl;
return 4;
}
//signal(SIGCHLD, SIG_IGN);//子进程退出后会自动释放资源,Linux下才能使用
for(; ;)
{
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
int new_sock = accept(listen_sock, (struct sockaddr*)&perr, &len);
if(new_sock < 0)
{
continue;
}
cout << "get a new link ...-> " << new_sock << endl;
pid_t id = fork();
if(id < 0)
{
continue;
}
else if(id == 0) //曾经被父进程打开的fd,会被子进程继承
{
//child
close(listen_sock);
if(fork() > 0) //子进程创建子进程
{
exit(0);//子进程退出
}
//向后走的进程是孙子进程
ServiceIO(new_sock);
close(new_sock);
exit(0); //实际上孙子进程就成了孤儿进程,被OS领养了
}
else
{
//父进程不要关闭listen_sock
close(new_sock);//父进程不需要,new_sock是给子进程使用
waitpid(id, nullptr, 0);//这里等待不会被阻塞,因为子进程被创建后立马退出了
}
//version 1: 单进程版,没人使用
//ServiceIO(new_sock);
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << " prot" << endl;
}
void ServiceIO(int new_sock)
{
//提供服务,我们是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));//空间初始化为0
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;//将获取的内容当成字符串
cout << "client# " << buffer << endl;
string echo_hello = ">>>server<<<: ";
echo_hello += buffer;
write(new_sock, echo_hello.c_str(), echo_hello.size());
}
else if(s == 0)
{
cout << "client quit ..." << endl;
break;
}
else
{
cerr << "read error" << endl;
break;
}
}
}
void* HandlerRequest(void* args)
{
pthread_detach(pthread_self());
int sock = *(int*)args;
delete (int*)args;
ServiceIO(sock);
close(sock); //线程关闭sock后,主线程就不用关闭了,他们是共享的!
}
// .tcp_server 8081
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//tcp server
//1、创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0)
{
cerr << "socket error: " << errno << endl;
return 2;
}
//2、bind 绑定
struct sockaddr_in local;
local.sin_family = AF_INET; //协议种类
local.sin_port = htons(atoi(argv[1])); //端口号
local.sin_addr.s_addr = INADDR_ANY; //ip
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error: " << errno << endl;
return 3;
}
//3、因为tcp是面向连接的,a. 在通信前,需要建立连接 b. 然后才能通信
//一定有人主动建立(客户端,需要服务的一方建立连接),
//一定有人被动接收连接(服务端,童工服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//设置套接字是listen状态,本质是允许用户连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0)
{
cerr << "listen error: " << errno << endl;
return 4;
}
for(; ;)
{
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
int new_sock = accept(listen_sock, (struct sockaddr*)&perr, &len);
if(new_sock < 0)
{
continue;
}
uint16_t cli_port = ntohs(perr.sin_port); //网络转主机序列,并考虑大小端
string cli_ip = inet_ntoa(perr.sin_addr); //将整型的IP转换为字符串风格的IP
cout << "get a new link ...-> [" << cli_ip << ":" << cli_port << new_sock << endl;
//曾经主线程打开的fd,新线程能看到,是共享的,共享文件描述符表
pthread_t tid;
int* pram = new int(new_sock);
pthread_create(&tid, nullptr, HandlerRequest, (void*)pram);//创建一个线程
//pthread_join(tid, nullptr);
//pthread_detach(tid); //这样写更好
}
return 0;
}
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.cc
g++ -o $@ $^ -std=c++11
tcp_server:tcp_server.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcp_client tcp_server
#pragma once
#include
#include
#include
using namespace std;
namespace ns_task
{
class Task
{
public:
Task()
{}
Task(int sock)
:_sock(sock)
{}
int Run()
{
//提供服务,我们是一个死循环
//while(true)
//{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(_sock, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = 0; //将获取的内容当成字符串
cout << "client# " << buffer << endl; //输出从客户端读到的内容
//拉取逻辑
string echo_string = ">>>server<<<: ";
echo_string += buffer;
write(_sock, echo_string.c_str(), echo_string.size());
}
else if(s == 0)
{
cout << "client quit ..." << endl;
}
else
{
cerr << "read error" << endl;
}
//}
close(_sock); //处理完任务关闭文件描述符
}
~Task()
{}
private:
int _sock;
};
}
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << "server_ip server_prot" << endl;
}
// ./tcp_client server_ip server_proc
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
//从命令行上获取的
string svr_ip = argv[1];
uint16_t svr_port = atoi(argv[2]);
//1、创建socket
int sock = socket(AF_INET, SOCK_STREAM/*stream流*/, 0);
if(sock < 0)
{
cerr << "socket error!" << endl;
return 2;
}
//2、bind 3、listen 4、accept
//client无需显示的bind,client->server
//client->connect!
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;//协议种类
//该函数做两件事情
//1、将点分十进制的字符串风格的IP,转换成为4字节IP
//2、将4byte由主机序列转换成为网络序列
server.sin_addr.s_addr = inet_addr(svr_ip.c_str()); //server ip
server.sin_port = htons(svr_port);
//2、发起连接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
cout << "connect server failed!" << endl;
return 3;
}
cout<< "connect success!" << endl;
//进行正常的业务请求了
while(true)
{
cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
//buffer初始化为0时米酒可以用sizeof了
write(sock, buffer, strlen(buffer)); //不要用sizeof(buffer),不一定是写1024字节
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
cout << "server echo# " << buffer << endl;
}
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"task.hpp"
#include"thread_pool.hpp"
using namespace std;
using namespace ns_task;
using namespace ns_threadpool;
void Usage(string proc)
{
cout << "Usage: " << proc << " prot" << endl;
}
// .tcp_server 8081
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//tcp server
//1、创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0)
{
cerr << "socket error: " << errno << endl;
return 2;
}
//2、bind 绑定
struct sockaddr_in local;
local.sin_family = AF_INET; //协议种类
local.sin_port = htons(atoi(argv[1])); //端口号
local.sin_addr.s_addr = INADDR_ANY; //ip
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error: " << errno << endl;
return 3;
}
//3、因为tcp是面向连接的,a. 在通信前,需要建立连接 b. 然后才能通信
//一定有人主动建立(客户端,需要服务的一方建立连接),
//一定有人被动接收连接(服务端,童工服务)
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//设置套接字是listen状态,本质是允许用户连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0)
{
cerr << "listen error: " << errno << endl;
return 4;
}
for(; ;)
{
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
int new_sock = accept(listen_sock, (struct sockaddr*)&perr, &len);
if(new_sock < 0)
{
continue;
}
uint16_t cli_port = ntohs(perr.sin_port); //网络转主机序列,并考虑大小端
string cli_ip = inet_ntoa(perr.sin_addr); //将整型的IP转换为字符串风格的IP
cout << "get a new link ...-> [" << cli_ip << ":" << cli_port << new_sock << endl;
Task t(new_sock); //将接收到客户端的sock交给任务构造,以便线程处理任务
ThreadPool::GetInstance()->PushTask(t); //任务放入线程池,这时候可以t,Run了
}
return 0;
}
#pragma once
#include
#include
#include
#include
#include
using namespace std;
namespace ns_threadpool
{
const int g_num = 5; //创建线程数量
template
class ThreadPool
{
private:
int _num;
queue _task_queue; //该成员是一个临界资源
pthread_mutex_t _mtx;
pthread_cond_t _cond;
static ThreadPool* ins;
private:
//构造函数必须得实现,但是必须要私有化
ThreadPool(int num = g_num)
: _num(num)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool& tp) = delete;
//赋值语句
ThreadPool operator=(const ThreadPool& tp) = delete;
public:
static ThreadPool* GetInstance()
{
//静态锁不用手动初始化和销毁
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//当前单列对象还没有被创建
if(ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率
{
pthread_mutex_lock(&lock);
if(ins == nullptr)
{
ins = new ThreadPool();
ins->InitThreadPool(); //构造对象后初始化
cout << "首次加载对象" << endl;
}
pthread_mutex_unlock(&lock);
}
return ins;
}
void Lock()
{
pthread_mutex_lock(&_mtx);
}
void Unlock()
{
pthread_mutex_unlock(&_mtx);
}
void Wait()
{
pthread_cond_wait(&_cond, &_mtx);
}
void Wakeup()
{
pthread_cond_signal(&_cond);
}
bool IsEmpty()
{
return _task_queue.empty();
}
public:
//在类中要让线程执行类内成员方法(参数个数匹配问题),是不可行的!
//必须让线程执行静态方法
static void* Rountine(void *args) //传this指针
{
pthread_detach(pthread_self()); //线程分离
ThreadPool* tp = (ThreadPool*)args;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
//任务队列为空,线程该做些什么呢?
tp->Wait();
}
//到这里,该任务队列中一定有任务了
T t;
tp->PopTask(&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 PushTask(const T &in)
{
Lock();
_task_queue.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out = _task_queue.front();
_task_queue.pop(); //外部往线程池放任务,剩余的它会解决,Task()是处理方式
}
~ThreadPool()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
};
template
ThreadPool* ThreadPool::ins = nullptr;
}
这里我们在处理任务时没有写死循环,所以任务只处理一次,剩下的就不在处理了,但是链接还在。
1、创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
2、绑定ip和端口号
bind(listen_sock, (struct sockaddr*)&local, sizeof(local))
3、设置监听套接字
listen(listen_sock, back_log)
4、接收请求
int new_sock = accept(listen_sock, (struct sockaddr*)&perr, &len)
5、提供服务
......
1、创建套接字
int sock = socket(AF_INET, SOCK_STREAM/*stream流*/, 0)
2、发起链接
connect(sock, (struct sockaddr*)&server, sizeof(server))
3、业务请求
......
可能博主没有对代码一行一行分析,但是如果大家有什么看不懂的地方或者我有错误的地方的话,欢迎大家私信我~
看到这里,给博主点个赞吧~