主页 > 创业  > 

网络套接字补充——TCP网络编程

六、TCP网络编程 6.1IP地址字符串和整数之间的转换接口 //字符串转整数接口 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); int inet_pton(int af, const char *strptr, void *addrptr);//注意dst指的是in_addr *的地址 in_addr_t inet_addr(const char *cp);//将字符串转32位并且是网络序列的; //整数转字符串 char *inet_ntoa(struct in_addr in);//将整数转为字符串并且将网络字节序转为主机字节序 const char *inet_ntop(int af, const void *addrptr, char *strptr, socklen_t size);

​ 需要注意的是inet_ntoa函数这个函数返回的是一个静态变量地址,使用时有覆盖问题和线程安全问题;最好是使用inet_ntop;

6.2补充知识

1.将网络套接字进行封装

​ 构造函数之中最好少做一些有风险的事情,这样可以保证最起码对象是没有问题的;其他如打开文件之类的操作就交给其他函数去完成;

2.获取新连接会产生多个文件描述符

​ 服务器本地的文件描述符用来进行监听连接,获取新连接,真正进行IO通信的文件描述符是后生成的;这样既提高了服务器的并发度;

3.telnet的使用

​ 默认使用的就是TCP;

​ 使用ctrl+]进入,回车后进行输入(会自动在输入的文本后面加\r\n)会回显数据,q退出;

4.TCP连接中的异常问题

​ a.当客户端直接退出时,服务端就会读到0,此时需要关闭为客户端打开的文件描述符;

​ b.读端关闭,写端继续写会导致操作系统发送SIGPIPE信号,然后出现杀死服务器的情况,所以一般要将13号信号忽略; 5.网络抖动断开连接,客户端自动发起连接请求设计

​ 长服务不合理,但是可以将客户端设置成循环发起连接;当网络出错会发生读写错误,这时候就可以进行对服务器重连;

int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); return 0; } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); // 发起连接请求 // 设置服务器套接字 sockaddr_in server; bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); while (true) { int cnt = 5; bool isreconnect = false; // 1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { cerr << "sockket" << endl; return 1; } do { // 2.发起连接 if (connect(sockfd, (sockaddr *)&server, sizeof(server)) < 0) { isreconnect = true; cnt--; cerr << "connect error, reconnect count: " << 5 - cnt << endl; sleep(1); } else if (isreconnect) { isreconnect = false; cout << "connect success" << endl; } } while (isreconnect && cnt); if (isreconnect == true) { cout << "reconnect fail" << endl; close(sockfd); return 1; } // 3.产生数据并且发送数据 string message; cout << "Please Enter@ "; getline(cin, message); if (write(sockfd, message.c_str(), message.size()) < 0) { cerr << "write error" << endl; isreconnect = true; continue; } char buff[4096]; int n = read(sockfd, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = 0; cout << buff << endl; } close(sockfd); } return 0; }

6.tcp服务器重连

​ 服务器断开后不能直接连接,一般要等待120s左右;

6.3使用接口 6.3.1创建套接字

​ 和udp使用是一样的;

6.3.2绑定套接字

​ 和udp使用是一样的;

6.3.3设置监听

​ 由于TCP是面向连接的,所以在通信前要建立连接,将套接字设置为监听状态;

#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog); //第二个参数表示的是全连接的队列的长度,一般不能设置的太大; 6.3.4获取新连接

​ 此处包括以上接口都是阻塞的;

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回值,成功返回一个文件描述符,失败返回-1,错误码被设置;

​ 获取新连接成功后要根据客户端的套接字信息提供服务;

6.3.5客户端发起连接请求

​ 客户端需要绑定但是不需要显式进行绑定,系统会在客户端发起连接请求的时候自动绑定;

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 6.4查看网络状态 netstat -nltp #n显示成数字,l表示listen,表示tcp 6.5单进程版echo服务器

​ 缺陷是同时最多只能有一个客户端进行访问;UDP所有的客户端用的是一个sockfd,一个文件,可以同时读写,而TCP每个客户端对应一个sockfd,一个文件;单进程下对一个文件读写时,服务器因为处理消息是循环处理,必须读完退出循环服务,才能继续获取新连接,此时另一个客户端已经想打开的sockfd文件写入很多数据,当服务端接收连接请求时,会将发送过来的一大批数据处理后返回,这样就无法实现正常的服务器;

char buff[4096]; while (true) { // 数据读取 ssize_t n = read(sockfd, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = '\0'; std::cout << "client say@ " << buff << std::endl; // 数据回显 std::string echo_string; echo_string += "tcpserver say#"; echo_string += buff; write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0) { lg(Info, "%s:%d quit..., server close sockfd: %d", clientip.c_str(), clientport, sockfd); break; } else { lg(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport); break; } } 6.6多进程版echo服务器

​ 1.子进程可以看见listenfd_,所以要关闭无关的文件描述符;2.父进程不关心sockfd,要去接收新的连接,如果不关闭就会导致之后的很多文件描述符没有关闭,不断从新的下标打开文件描述符,而不是重新分配;

​ 2.子进程中继续fork(),然后子进程退出,被父进程阻塞等待回收,父进程继续获取新的连接,孙子进程被操作系统领养,执行服务部分;也可以使用信号异步等待的方式实现;

​ 3.也可以在循环执行获取连接和执行任务之前创建子进程,但是会存在数据不一致问题需要用信号量;

​ 4.多进程创建的成本过高,所以应该选择多线程;

//方式1 pid_t id = fork(); if (id < 0) { std::cerr << "fork error" << std::endl; } else if (id == 0) { close(listensockfd_); if (fork() > 0) { exit(0); } // 此处执行代码的是孙子进程,会被做系统领养 service(sockfd, clientip, clientport); close(sockfd); exit(0); } close(sockfd); pid_t rid = waitpid(id, nullptr, 0); (void)rid; //方式2 signal(SIGCHLD, SIG_IGN); pid_t id = fork(); if (id == 0) { close(listensockfd_); service(sockfd, clientip, clientport); close(sockfd); exit(0); } close(sockfd); 6.7多线程版本的echo服务器

​ 1.因为线程中大部分资源都是共享的所以不可以关闭文件描述符,否则会出错;

​ 2.线程没有退出时会有峰值的,服务器此时压力很大,所以长服务是不合理的;

​ 3.创建线程也是有成本的,即系统调用的成本,所以应该用线程池;

struct threaddata { threaddata(const int sockfd, const std::string &clientip, const uint16_t &clientport, tcpserver *t) : sockfd_(sockfd), clientport_(clientport), clientip_(clientip), t_(t) { } int sockfd_; uint16_t clientport_; std::string clientip_; tcpserver *t_; }; pthread_t tid; threaddata *td = new threaddata(sockfd, clientip, clientport, this); pthread_create(&tid, nullptr, routine, (void *)td); static void *routine(void *args) { threaddata *td = static_cast<threaddata *>(args); pthread_detach(pthread_self()); td->t_->service(td->sockfd_, td->clientip_, td->clientport_); delete td; return nullptr; } 6.8线程池版本的echo服务器

​ 1.线程池里不可以执行长时间的服务;2.服务器关闭了客户端套接字,客户端继续写入,会触发服务器异常,返回一个RST消息,然后客户端操作系统发送SIGPIPE信号杀死客户端进程;

class Task { public: Task(const int &sockfd, const std::string &clientip, const uint16_t &clientport) : sockfd_(sockfd), clientport_(clientport), clientip_(clientip) {} void run() { char buff[4096]; // 数据读取 ssize_t n = read(sockfd_, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = '\0'; std::cout << "client say@ " << buff << std::endl; // 数据回显 std::string echo_string; echo_string += "tcpserver say#"; echo_string += buff; write(sockfd_, echo_string.c_str(), echo_string.size()); } else if (n == 0) { lg(Info, "%s:%d quit..., server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_); } else { lg(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd_, clientip_.c_str(), clientport_); } close(sockfd_); } void operator()() { run(); } ~Task() { } private: int sockfd_; uint16_t clientport_; std::string clientip_; }; Task t(sockfd, clientip, clientport); ThreadPool<Task>::GetInstance()->Push(t); #include <iostream> #include <vector> #include <string> #include <queue> #include <pthread.h> #include "Task.hpp" #include <unistd.h> struct ThreadInfo { pthread_t tid; std::string name; }; static const int defaultnum = 5; template <class T> class ThreadPool { private: void Lock() { pthread_mutex_lock(&_mutex); } void UnLock() { pthread_mutex_unlock(&_mutex); } void Wakeup() { pthread_cond_signal(&_cond); } void ThreadSleep() { pthread_cond_wait(&_cond, &_mutex); } bool IsQueueEmpty() { return _tasks.empty(); } std::string GetThreadName(pthread_t tid) { for (const auto e : _threads) { if (e.tid == tid) { return e.name; } } return "None"; } public: T Pop() { T t = _tasks.front(); _tasks.pop(); return t; } void Push(const T &t) { Lock(); _tasks.push(t); Wakeup(); UnLock(); } static void *HandlerTask(void *args) // 类内函数默认都有一个this指针,静态成员函数无法直接看到成员属性 { ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args); std::string name = tp->GetThreadName(pthread_self()); while (true) { tp->Lock(); while (tp->IsQueueEmpty()) { tp->ThreadSleep(); } Task t = tp->Pop(); tp->UnLock(); t(); } } void Start() // 创建线程 { int num = _threads.size(); for (int i = 0; i < num; i++) { _threads[i].name = "thread-" + std::to_string(i + 1); pthread_create(&(_threads[i].tid), nullptr, HandlerTask, this); } } static ThreadPool<T> *GetInstance() { pthread_mutex_lock(&_smutex); if (_tp == nullptr) { std::cout << "log : singleton create done first!" << std::endl; _tp = new ThreadPool<T>(); } pthread_mutex_unlock(&_smutex); return _tp; } private: ThreadPool(int num = defaultnum) : _threads(num) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); } ThreadPool(const ThreadPool<T> &tp) = delete; const ThreadPool<T> &operator=(const ThreadPool<T> tp) = delete; ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } std::vector<ThreadInfo> _threads; std::queue<T> _tasks; pthread_mutex_t _mutex; pthread_cond_t _cond; static ThreadPool<T> *_tp; static pthread_mutex_t _smutex; }; template <class T> ThreadPool<T> *ThreadPool<T>::_tp = nullptr; template <class T> pthread_mutex_t ThreadPool<T>::_smutex = PTHREAD_MUTEX_INITIALIZER; 6.9线程池版翻译服务器

​ 打开KV式的字符串文件,来比较进行翻译;

#include <iostream> #include <string> #include <unordered_map> #include <fstream> #include <cstring> #include "log.hpp" extern Log lg; const std::string filename = "./dict.txt"; // 打开字典并且自动将初始化dict class init { public: bool split(const std::string &line, std::string &part1, std::string &part2) { auto pos = line.find(sep); if (pos == std::string::npos) { return false; } part1 = line.substr(0, pos); part2 = line.substr(pos + sep.size()); return true; } init() { // 1.打开文件 std::ifstream in(filename); // 默认打开文件 if (!in.is_open()) { lg(Fatal, "open %s file error, errno: %d, strerror: %s", filename.c_str(), errno, strerror(errno)); exit(4); } std::string line; // 2.对文件进行按行读取 while (std::getline(in, line)) { std::string part1, part2; split(line, part1, part2); dict_[part1] = part2; } // 3.关闭文件 in.close(); } std::string translation(const std::string &key) { auto it = dict_.find(key); if (it != dict_.end()) { return dict_[key]; } else { return "unknown"; } } private: std::unordered_map<std::string, std::string> dict_; static const std::string sep; }; const std::string init::sep = ": "; // 处理任务 buff[n] = '\0'; // 当作字符串使用 std::string echo_string; echo_string += it.translation(buff); write(sockfd_, echo_string.c_str(), echo_string.size()); 七、对服务守护进程化 7.1会话session相关概念

​ 在Linux系统中,用户进行登陆的时候,操作系统会为用户形成一个会话(session);为每一个会话创建一个bash进程,来为用户提供命令行的服务,这个bash与键盘和显示器是直接相关的;这个bash默认是前台进程,而一个会话最多只能有一个前台进程,但是可以有多个后台进程;

​ 键盘信号只能发送给前台进程,bash对键盘信号做了特殊处理无法直接ctrl+c杀死;

​ 当直接运行程序时形成的进程默认是前台进程,可以被键盘信号直接俄杀死,但是在命令行最后加上&就会形成后台进程,bash默认变为前台进程,而后台进程无法获取键盘信号,会一直执行;

​ 前台进程和后台进程都可以正常运行;但是后台进程运行不影响前台输入命令,这样提高了并发度;前台和后台进程的区别就是只有前台进程才可以获取键盘输入信息;

​ 为了保证总有进程可以键盘输入,所以前台进程是必须一直存在;

7.2会话相关操作

​ 注意是在当前会话进行操作,其他会话是看不到的;

1.前台进程

./a.out #前台任务的方式运行程序;

2.后台进程

./a.out & #后台任务的方式运行程序;

3.fg

fg 任务号 #将任务号对应的后台任务提到前台并唤醒,方便用ctrl+c杀死进程; #任务号默认从1开始,之后每次创建一个后台程序,任务号就++;

4.jobs

jobs #查看当前会话的后台任务;

5.ctrl+z

ctrl+z #将前台任务暂停,因为暂停后的组内进程没有意义的,导致没人获取键盘,所以会被放到后台去,bash提到前台

6.bg

bg 任务号 #将后台暂停的任务唤醒 7.3Linux中进程间关系

​ 即进程之间除了独立的关系外还构成组的关系;进程组来负责完成一个任务;

​ 操作系统要将会话进行管理,登陆时创建了session的结构体来描述会话的信息;通过sid来标识会话的唯一性,sid为bash进程pid;

​ 单进程的任务是自成一个进程组;多进程的任务,父进程是组长,构成一个进程组;进程组以组长的pid为进程组号;

​ 当会话关闭时,bash会释放(前台任务会关闭),但是后台任务确实没有释放;由于bash是后台任务的父进程,直接释放会导致执行后台任务的进程组内进程托孤给操作系统,需要注意的是ppid变成了1,与终端没有关系了;其他属性也被保留;这就会导致后台任务收到会话登录和退出的影响;

​ Windows中的用户在退出(注销)时会将会话内的所有的进程全部关闭;

7.4守护进程化

​ 为了使得进程不被会话登录和退出影响,需要进行守护进程化;即让一个进程自成一个会话,不需要和键盘显示器进行关联,自成进程组自成会话;

​ 守护进程的本质也是孤儿进程;

​ 守护进程不需要使用显示器和键盘文件了;但是也不可以将这些文件关闭,防止后续使用出错 ;最好的方式就是重定向到**/dev/null**这个垃圾桶,所有的内容都会被自动丢弃;这样就实现了远程服务的效果;

​ 守护进程一般以d结尾;

#include <unistd.h> pid_t setsid(void); //进程组组长不可以守护进程化,所以需要子进程来执行任务; //1.自己实现守护进程 void Daemon(const std::string &cwd = "") { // 1.忽略异常信号 signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN); signal(SIGSTOP, SIG_IGN); // 2.变成独立的会话 if (fork() > 0) exit(0); setsid(); // 3.更改调用进程的当前工作目录 if (!cwd.empty()) { chdir(cwd.c_str()); } // 4.将标准输入、标准输出、标准错误重定向到/dev/null中,或者写到日志当中去 int fd = open("/dev/null", O_RDWR); if (fd > 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); } } //2.系统调用守护进程 #include <unistd.h> int daemon(int nochdir, int noclose); //第一个参数为0表示将进程当前的工作目录改为根目录,第二个参数为.表示不将文件描述符重定向到\dev\null; 八、TCP三次握手和四次挥手

​ TCP协议在建立连接是通过三次握手,而释放连接使用的是四次挥手;建立连接成功之前accept会阻塞,成功之后才会返回;关闭一次文件就是两次挥手,双方都是要关闭文件的所以共挥手四次;

​ TCP和UDP都是全双工通信的;TCP底层创建了两个缓冲区,一个是发送缓冲区,一个是接收缓冲区;即一个文件描述符对应两个文件缓冲区,所以通信时全双工的,读和写是不会互相影响的;UDP没有对应的发送缓冲区;

​ 操作系统要对连接进行管理,创建结构体对象进行描述,然后用链表进行管理;

标签:

网络套接字补充——TCP网络编程由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“网络套接字补充——TCP网络编程