赞
踩
UDP和TCP的区别:
对于TCP协议有几个特点:
1️⃣ 传输层协议
2️⃣ 有连接(正式通信前要先建立连接)
3️⃣ 可靠传输(在内部帮我们做可靠传输工作)
4️⃣ 面向字节流
对于UDP协议有几个特点:
1️⃣ 传输层协议
2️⃣ 无连接
3️⃣ 不可靠传输
4️⃣ 面向数据报
可以看到TCP对比UDP会建立链接。
其他的接口跟UDP其实没什么区别:【网络编程】demo版UDP网络服务器实现
在通信之前要先把网卡文件打开。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
RETURN VALUE
On success, a file descriptor for the new socket is returned.
On error, -1 is returned, and errno is set appropriately.
这个函数的作用是打开一个文件,把文件和网卡关联起来。
参数介绍:
domain
:一个域,标识了这个套接字的通信类型(网络或者本地)。
只用关注上面两个类,第一个AF_UNIX
表示本地通信,而AF_INET
表示网络通信。
type
:套接字提供服务的类型。
这一章我们讲的式TCP,所以使用SOCK_STREAM
。
protocol
:想使用的协议,默认为0即可,因为前面的两个参数决定了,就已经决定了是TCP还是UDP协议了。
返回值:
成功则返回打开的文件描述符(指向网卡文件),失败返回-1。
而从这里我们就联想到系统中的文件操作,未来各种操作都要通过这个文件描述符,所以在服务端类中还需要一个成员变量表示文件描述符。
#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include "log.hpp" class TCPServer { static const uint16_t gport = 8080; public: TCPServer(cosnt uint16_t& port = gport) : _sock(-1) , _port(port) {} void InitServer() { _sock = socket(AF_INET, SOCK_STREAM, 0); if(_sockfd == -1) { std::cerr << "create socket error" << std::endl; exit(1); } std::cout << "create socket success" << std::endl; } void start() {} private: int _sock; uint16_t _port; };
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
RETURN VALUE
Upon successful completion, bind() shall return 0;
otherwise, -1 shall be returned and errno set to indicate the error.
参数介绍:
socket
:创建套接字的返回值。
address
:通用结构体(【网络编程】socket套接字有详细介绍)。
address_len
:传入结构体的长度。
所以我们要先定义一个sockaddr_in
结构体填充数据,在传递进去。
然后就是跟UDP一样,先初始化结构体,再处理IP和端口。
要注意IP要绑定任意IP也就是INADDR_ANY
。
至于为什么再上一章【网络编程】demo版UDP网络服务器实现有过详细讲解。
void InitServer() { _sock = socket(AF_INET, SOCK_STREAM, 0); if(_sockfd == -1) { std::cerr << "create socket error" << std::endl; exit(1); } std::cout << "create socket success" << std::endl; struct sockaddr_in si; // 初始化结构体 bzero(&si, sizeof si); si.sin_family = AF_INET; si.sin_port = htons(_port);// 主机转网络序列 si.sin_addr.s_addr = INADDR_ANY; if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0) { std::cout << "bind socket error" << std::endl; exit(1); } std::cout << "bind socket success" << std::endl; }
TCP跟UDP的不同在这里就体现了出来。
要把socket套接字的状态设置为listen状态。只有这样才能一直获取新链接,接收新的链接请求。
举个例子:
我们买东西如果出现了问题会去找客服,如果客服不在那么就回复不了,所以规定了客服在工作的时候必须要时刻接收回复消息,这个客服所处的状态就叫做监听状态。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
RETURN VALUE
On success, zero is returned.
On error, -1 is returned, and errno is set appropriately.
关于第二个参数backlog后边讲TCP协议的时候介绍,目前先直接用。
static const int gbacklog = 10;
void InitServer() { _sock = socket(AF_INET, SOCK_STREAM, 0); if(_sock == -1) { std::cerr << "create socket error" << std::endl; exit(1); } std::cout << "create socket success" << std::endl; struct sockaddr_in si; // 初始化结构体 bzero(&si, sizeof si); si.sin_family = AF_INET; si.sin_port = htons(_port);// 主机转网络序列 si.sin_addr.s_addr = INADDR_ANY; if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0) { std::cout << "bind socket error" << std::endl; exit(1); } std::cout << "bind socket success" << std::endl; // 设置监听状态 if(listen(_sock, gbacklog) < 0) { std::cout << "listen socket error" << std::endl; exit(1); } std::cout << "listen socket success" << std::endl; }
上面初始化完毕,现在开始就是要运行服务端,而TCP不能直接发数据,因为它是面向链接的,必须要先建立链接。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
RETURN VALUE
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket.
On error, -1 is returned, and errno is set appropriately.
参数介绍:
sockfd
文件描述符,找到套接字
addr
输入输出型参数,是一个结构体,用来获取客户端的信息。
addrlen
输入输出型参数,客户端传过来的结构体大小。
返回值:
成功返回一个文件描述符
失败返回-1
而我们知道sockfd本来就是一个文件描述符,那么这个返回的文件描述符是什么呢?
举个例子:
我们去吃饭的时候会发现每个店铺门口都会有人来招揽顾客,这个人把我们领进去门店后,然后他就会继续站在门口继续招揽顾客,而我们会有里面的服务员来招待我们,给我们提供服务。
这里的揽客的人就是sockfd,而服务员就是返回值的文件描述符。
意思就是sockfd的作用就是把链接从底层获取上来,返回值的作用就是跟客户端通信。
从这里就知道了成员变量中的_sock
并不是通信用的套接字,而是获取链接的套接字。为了方便观察,我们可以把所有的_sock换
成_listensock
。
void start() { while(1) { // 获取新链接 struct sockaddr_in si; socklen_t len = sizeof si; int sock = accept(_listensock, (struct sockaddr*)&si, &len); if(sock < 0) { // 获取链接失败无影响,继续获取即可 std::cout << "accept error, continue" << std::endl; continue; } std::cout << "accept a new link success" << std::endl; std::cout << "sock: " << sock << std::endl; } }
上面获取到了通信用的套接字sock,而因为TCP通信是面向字节流的,所以后续通信全部是用文件操作(IO),因为文件也是面向字节流的。
IO的操作我们可以封装一个函数。
void ServerIO(int sock) { char buf[1024]; // 接收消息 while(1) { ssize_t n = read(sock, buf, sizeof buf - 1); if(n > 0) { buf[n] = '\0'; std::cout << "read a message: " << buf << std::endl; // 把消息发送回去 std::string outbuf; outbuf += "Server[echo]#"; outbuf += buf; write(sock, outbuf.c_str(), outbuf.size()); } else if(n == 0) { // 代表客户端退出 std::cout << "Client quit" << std::endl; break; } } }
当IO完后要记得关闭文件描述符sock,不然会导致可用描述符越来越少。
验证发现可以运行
Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中。
所以客户端也需要一个套接字。
void initClient()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
std::cout << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
}
跟上一章一样要绑定,但是不能显示绑定。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
RETURN VALUE
If the connection or binding succeeds, zero is returned.
On error, -1 is returned, and errno is set appropriately.
参数说明:
这里的
addr
和addrlen
填入的是服务端信息。
在UDP通信中,客户端在sendto的时候会自动绑定IP和port,TCP这里就是在connect的时候绑定。
void start() { struct sockaddr_in si; bzero(&si, sizeof si); si.sin_family = AF_INET; si.sin_port = htons(_serverport); si.sin_addr.s_addr = inet_addr(_serverip.c_str()); if(connect(_sock, (struct sockaddr*)&si, sizeof si) < 0) { std::cout << "connect socket error" << std::endl; } else { std::string msg; while(1) { std::cout << "Please Enter#"; std::getline(std::cin, msg); write(_sock, msg.c_str(), msg.size()); // 从服务端读取数据 char buf[1024]; int n = read(_sock, buf, sizeof buf - 1); if(n > 0) { buf[n] = '\0'; std::cout << buf << std::endl; } else { break; } } } }
最后在析构的时候要关掉文件描述符。
~TCPClient()
{
if(_sock >= 0)
{
close(_sock);
}
}
服务端:
客户端:
但此时如果我们在用一个客户端链接,会发现无法通信,除非第一个客户端退出。
这是因为获取新链接后会进入ServerIo中死循环。只要这个客户端不退出,就会一直给这个客户端提供服务。
那怎么保证多个客户端并行呢?
因为fork后子进程会复制父进程的文件描述符。
这里注意子进程并不需要_listensock
文件描述符,所以最好关闭。
pid_t id = fork();
if(id == 0)// child
{
close(_listensock);
ServerIO(sock);
close(sock);
exit(1);
}
接下来父进程怎么办呢?是等待吗?
如果父进程等待的话又会导致上面的情况,子进程不退出父进程就一直等待。
子进程退出时,会给父进程发送一个SIGCHLD,17号信号。所以有一种解决办法就是用signal
函数,在回调函数中把waitpid的参数设置为-1(等待任意进程),就可以回收。
现在我们不用这种办法,我们可以这么写:
pid_t id = fork();
if(id == 0)// child
{
close(_listensock);
if(fork() > 0) exit(1);
ServerIO(sock);
close(sock);
exit(1);
}
// father
pid_t ret = waitpid(id, nullptr, 0);
if(ret > 0)
{
std::cout << "wait success" << ret << std::endl;
}
这里的意思就是创建孙子进程,父进程直接退出,让孙子进程执行ServerIO,此时孙子进程就会被操作系统收养,不用我们管,而父进程退出,外边的父进程也等待成功了。
结果演示:
客户端:
服务端:
其实下面的等待可以不用等待,因为SIGCHLD信号默认的处理方式是忽略。
这里看到客户端退出了但是文件描述符并没有被回收。
这里的原因是我们只关闭了子进程的文件描述符,没有关闭父进程:
struct ThreadData { TCPServer* _self; int _sock; }; void start() { while(1) { // 获取新链接 struct sockaddr_in si; socklen_t len = sizeof si; int sock = accept(_listensock, (struct sockaddr*)&si, &len); if(sock < 0) { // 获取链接失败无影响,继续获取即可 std::cout << "accept error, continue" << std::endl; continue; } std::cout << "accept a new link success" << std::endl; std::cout << "sock: " << sock << std::endl; // 多线程 pthread_t tid; ThreadData* td = new ThreadData({this, sock}); pthread_create(&tid, nullptr, thread_start, td); } } static void* thread_start(void* args) { // 线程分离 pthread_detach(pthread_self()); ThreadData* tp = static_cast<ThreadData*>(args); tp->_self->ServerIO(tp->_sock); close(tp->_sock); delete tp; }
前面我们写过线程池【linux】基于单例模式实现线程池,这里直接拿来用即可。
这里就要修改一下代码,因为ServerIO可以不属于类,所以可以把ServerIO放在任务Task.hpp
中。
// Task.hpp #pragma once #include <iostream> #include <string> #include <functional> #include <cstdio> void ServerIO(int sock) { char buf[1024]; // 接收消息 while(1) { ssize_t n = read(sock, buf, sizeof buf - 1); if(n > 0) { buf[n] = '\0'; std::cout << "read a message: " << buf << std::endl; // 把消息发送回去 std::string outbuf; outbuf += "Server[echo]#"; outbuf += buf; write(sock, outbuf.c_str(), outbuf.size()); } else if(n == 0) { // 代表客户端退出 std::cout << "Client quit" << std::endl; break; } } } class Task { typedef std::function<void(int)> func_t; public: Task() {} Task(int sock, func_t func) : _sock(sock) , _func(func) {} void operator()() { _func(_sock); } std::string tostringTask() { char buf[64]; snprintf(buf, sizeof buf, "%d %c %d = ?", _x, _op, _y); return buf; } private: int _sock; func_t _func; }; // ThreadPool.hpp #pragma once #include <vector> #include <queue> #include <mutex> #include "mythread.hpp" #include "mymutex.hpp" #include "Task.hpp" using std::cout; using std::endl; const int N = 5; template <class T> class ThreadPool; template <class T> struct ThreadData { ThreadPool<T>* _tp; std::string _name; ThreadData(ThreadPool<T>* tp, const std::string& name) : _tp(tp) , _name(name) {} }; template <class T> class ThreadPool { private: static void* handlerTask(void* args) { ThreadData<T>* tdp = static_cast<ThreadData<T>*>(args); while(true) { tdp->_tp->lockqueue(); while(tdp->_tp->isqueueempty()) { tdp->_tp->threadwait(); } T t = tdp->_tp->pop(); tdp->_tp->unlockqueue(); t(); } delete tdp; } void lockqueue() volatile { pthread_mutex_lock(&_mutex); } void unlockqueue() volatile { pthread_mutex_unlock(&_mutex); } bool isqueueempty() volatile { return _tasks.empty(); } void threadwait() volatile { pthread_cond_wait(&_cond, &_mutex); } T pop() volatile { T res = _tasks.front(); _tasks.pop(); return res; } ThreadPool(int num = 5) : _num(num) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); // 创建线程 for(int i = 0; i < _num; i++) { _threads.push_back(new Thread()); } } ThreadPool(const ThreadPool<T>& ) = delete; ThreadPool<T> operator=(const ThreadPool<T>&) = delete; public: void start() volatile { for(auto& t : _threads) { ThreadData<T>* td = new ThreadData<T>(this, t->GetName()); t->start(handlerTask, td); } } void push(const T& in) volatile { LockAuto lock(&_mutex); _tasks.push(in); // 唤醒池中的一个线程 pthread_cond_signal(&_cond); } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); for(auto & e : _threads) { delete e; } } volatile static ThreadPool<T>* GetSingle() { if(_tp == nullptr) { _singlelock.lock(); if(_tp == nullptr) { _tp = new ThreadPool<T>(); } _singlelock.unlock(); } return _tp; } private: int _num;// 线程数量 std::vector<Thread*> _threads; std::queue<T> _tasks;// 任务队列 pthread_mutex_t _mutex;// 保护任务队列 pthread_cond_t _cond; volatile static ThreadPool<T>* _tp; static std::mutex _singlelock; }; template <class T> volatile ThreadPool<T>* ThreadPool<T>::_tp = nullptr; template <class T> std::mutex ThreadPool<T>::_singlelock; // TCPServer.hpp #pragma once #include <iostream> #include <string> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <strings.h> #include <cstdlib> #include <sys/wait.h> #include <pthread.h> #include "ThreadPool.hpp" #include "log.hpp" class TCPServer; struct ThreadData { TCPServer* _self; int _sock; }; class TCPServer { static const uint16_t gport = 8080; static const int gbacklog = 10; public: TCPServer(const uint16_t& port = gport) : _listensock(-1) , _port(port) {} void InitServer() { _listensock = socket(AF_INET, SOCK_STREAM, 0); if(_listensock == -1) { std::cerr << "create socket error" << std::endl; exit(1); } std::cout << "create socket success" << std::endl; struct sockaddr_in si; // 初始化结构体 bzero(&si, sizeof si); si.sin_family = AF_INET; si.sin_port = htons(_port);// 主机转网络序列 si.sin_addr.s_addr = INADDR_ANY; if(bind(_listensock, (struct sockaddr*)&si, sizeof si) < 0) { std::cout << "bind socket error" << std::endl; exit(1); } std::cout << "bind socket success" << std::endl; // 设置监听状态 if(listen(_listensock, gbacklog) < 0) { std::cout << "listen socket error" << std::endl; exit(1); } std::cout << "listen socket success" << std::endl; } void start() { // 线程池初始化 ThreadPool<Task>::GetSingle()->start(); while(1) { // 获取新链接 struct sockaddr_in si; socklen_t len = sizeof si; int sock = accept(_listensock, (struct sockaddr*)&si, &len); if(sock < 0) { // 获取链接失败无影响,继续获取即可 std::cout << "accept error, continue" << std::endl; continue; } std::cout << "accept a new link success" << std::endl; std::cout << "sock: " << sock << std::endl; // 线程池 ThreadPool<Task>::GetSingle()->push(Task(sock, ServerIO)); // 多线程 // pthread_t tid; // ThreadData* td = new ThreadData({this, sock}); // pthread_create(&tid, nullptr, thread_start, td); // 多进程 // pid_t id = fork(); // if(id == 0)// child // { // close(_listensock); // if(fork() > 0) exit(1); // ServerIO(sock); // close(sock); // exit(1); // } // close(sock); // father // pid_t ret = waitpid(id, nullptr, 0); // if(ret > 0) // { // std::cout << "wait success " << ret << std::endl; // } // ServerIO(sock); // // 关闭使用完的文件描述符 // close(sock); } } static void* thread_start(void* args) { // 线程分离 pthread_detach(pthread_self()); ThreadData* tp = static_cast<ThreadData*>(args); tp->_self->ServerIO(tp->_sock); close(tp->_sock); delete tp; } private: int _listensock; uint16_t _port; };
对比UDP服务器,TCP服务器多了获取新链接和监听的操作,而因为TCP是面向字节流的,所以接收和发送数据都是IO操作,也就是文件操作。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。