学习任务:
继网络套接字(一),继续学习套接字socket编程接口(已经学习了socket和bind),实现TCP客户端/服务器(单连接版本, 多进程版本, 多线程版本,进程或线程池版本),并且理解tcp服务器建立连接, 发送数据, 断开连接的流程。
1.socket编程接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
接口解析
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
函数功能:由于服务器需要周而复始地等待客户端和自己建立连接,因此该函数由服务器使用,功能是等待用户连接,进行系统侦听请求。
第一个参数sockefd:由socket接口创建的套接字fd,不过需要注意
第二个参数backlog:套接字排队的最大连接个数(建议5~10),即申请连接的客户端的个数。
返回值:成功返回0,错误返回-1。
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
函数功能:接收用户连接请求,并返回一个新的套接字描述符用于与客户端通信。
第一个参数sockfd:由socket接口创建的套接字fd。
第二个参数addr:用于保存客户端的进程协议地址的结构体。
第三个参数addrlen:addr的大小。
返回值:返回一个新的套接字描述符。
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数功能:建立连接。
第一个参数sockfd:由accept接口创建的套接字描述符。
第二个参数addr:套接字地址结构的指针。
第三个参数addrlen:addr的大小。
返回值:成功返回0。
单进程版本TCP(客户端/服务器)
单进程版本没有人会去使用,因为这种版本只能是一对一的连接,很明显不能符合业务要求的,就好比我们打开一个学习软件去学习,同学A先打开了,那么同学B、C和更多的其他同学都不能打开了。这里我们借助单进程版本来学习。
首先是写出服务器的代码,代码的思路是这样的:
①首先为服务器创建套接字,因为这个是TCP协议,TCP是面向连接的,因此服务器是需要进入监听状态才能让客户端连接,所以使用socket接口创建出来的套接字是属于监听套接字,负责绑定IP和端口号,负责监听的。
②创建完监听套接字后,开始绑定IP和端口号。先创建出服务器的sockaddr_in结构(因为使用AF_INET协议),然后填充结构体,填充的是使用何种协议域,IP和端口号。在填充IP的时候,选择任意绑定IP。
③设置监听状态,监听状态的服务器,通俗地来解释就是服务器进入监听状态,就是告诉客户端我可以被连接了,来吧!
④使用accept接口,创建出提供服务的套接字。这里需要建立另外一个sockaddr_in结构体,这个结构体是保存客户端的ip和端口号。
⑤最后就是提供服务,由于TCP是面向字节流的,跟文件操作一样,因此我们可以使用文件操作进行读写。
注意:
在bind方法中的sockaddr结构体里面填充的是服务端的ip地址和端口号,bind就把服务器的ip地址和端口号和前面的监听套接字结合起来了。而accept方法中的socketaddr结构体保存的是客户端的ip地址和端口号信息。
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout 0)//读取成功
{
buffer[s] = 0;//将获取的内容当成字符串
std::cout>>server
客户端代码,代码思路如下:
①创建套接字。
②客户端不需要显示绑定ip和端口号。注意,是不需要显示绑定,并非不需要绑定,因为在客户端连接服务器的时候,操作系统会自动地绑定ip和端口号。如果固定地绑定,如果其它客户端随机绑定,随机到了我这个客户端的端口号此时我这个客户端就不能启动了。客户端关心的是连接处于监听状态的服务器。
③发起连接。先创建保存服务器ip地址和端口号信息的socketaddr结构体,然后使用connect方法进行连接。
④连接成功就可以开始通信了。
#include
#include
#include
#include
#include
#include
#include
// ./tcp_client server_ip server_port
void Usage(std::string proc)
{
std::cout0)
{
buffer[s]=0;
std::cout
多进程版本TCP(客户端/服务器)
由于单进程版没有人会去使用,所以我们通过单进程版本来学习一下简单的代码实现操作,接着我们对单进程版本改造(服务器的代码),变成多进程版本。
代码思路:让父进程创建子进程,子进程去执行网络通信,执行完后就把fd关掉。同时,进入到父进程,表示了子进程已经拿到了用于通信的套接字,那么父进程就可以它关闭掉。为了避免产生僵尸进程,使用自定义信号,让父进程忽略子进程退出的信号,让子进程自动退出并释放资源。
#include
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout 0)//读取成功
{
buffer[s] = 0;//将获取的内容当成字符串
std::cout>>server : ["
多线程版本TCP(客户端/服务器)
代码思路:让主线程创建新线程,然后让新线程分离,这样就可以不用线程等待,并且分离出去的线程带着用于通信的套接字去进行通信,通信完后就关闭fd。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout 0)//读取成功
{
buffer[s] = 0;//将获取的内容当成字符串
std::cout>>server : ["
警告:
在上面两个版本(多线程和多进程版本)的写法中看,有两个弊端:
①创建进程或线程是无上限的,这给了一些带有恶意的用户一个搞破坏的机会!
②每次都是要等待客户连接来了,才会去创建子进程或者是线程。这毫无疑问浪费了不必要的时间。
因此针对这两个问题,我们可以采用进程池或者是线程池来解决!下面使用线程池版本!
线程池版本TCP(客户端/服务器)
#pragma once
#include
#include
#include
#include
#include
namespace ns_threadpool
{
const int g_num = 5;
template
class ThreadPool
{
private:
int num_;
std::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=(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();
std::cout *tp = (ThreadPool *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpey())
{
//任务队列为空,线程该做什么呢??
tp->Wait();
}
//该任务队列中一定有任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t.Run();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i
ThreadPool *ThreadPool::ins = nullptr;
} // namespace ns_threadpool
#pragma once
#include
#include
#include
namespace ns_task
{
class Task
{
private:
int sock;
public:
Task() : sock(-1) {}
Task(int _sock) : sock(_sock) {}
int Run()
{
//提供服务,短服务
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout >>server
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"thread_pool.hpp"
#include"Task.hpp"
using namespace ns_threadpool;
using namespace ns_task;
void Usage(std::string proc)
{
std::cout : ["::GetInstance()->PushTask(t);
}
return 0;
}
TCP协议通信流程
通过上面的代码,我们来总结一下流程:
①创建监听套接字sock。socket()方法本质是打开文件,这是跟系统相关的。
②绑定ip和端口号,使用bind()方法,本质是ip+port+文件信息进行关联。
③服务端设置为监听状态,使用listen()方法,本质是设置该socket文件的状态,允许客户端来连接。
④获取连接,使用accept()方法,连接的本质是描述连接的结构体,由操作系统管理着。
⑤发起链接,使用connect()方法,本质是发起链接,在系统层面就是构建一个申请报文发送过去,在网络层面,发起tcp链接的三次握手!
⑥进行网络通信,使用文件读写的方式,read/wirte的方法。关
⑦闭监听套接字close(fd),本质:a、在系统层面,释放曾经申请的文件资源和连接资源等待。b、在网络层面,通知对方服务端的连接已经关闭。
⑧关闭用于通信的套接字close() && server/client,本质是在网络层面进行四次挥手!
三次握手:
在服务器建立连接的时候:
调用socket, 创建文件描述符。
调用connect, 向服务器发起连接请求。
connect会发出SYN段并阻塞等待服务器应答(第一次)。
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示”同意建立连接”(第二次)。
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段(第三次)。
四次挥手:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)客户端收到FIN, 再返回一个ACK给服务器; (第四次)。
在上面写的代码中,不管是在单进程、多进程多线程还是线程池,我们其实都是在使用系统调用,从零开始编写网络层,而非在使用网络层!
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net