Linux----网络编程socket

1)端口号

src:ip + src:port <-> dst:ip + dst:port (确定互联网中唯一程序 <-> 确定互联网中唯一程序)


端口号(port)是传输层协议的内容:

  1. 端口号是一个2字节16位的整数;
  2. 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  3. IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  4. 一个端口号只能被一个进程占用

"端口号" 和 "进程ID":

  • 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

源端口号和目的端口号:

  • 传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁

2)初识TCP/UDP协议

TCP:

  1. 传输层协议
  2. 有连接
  3. 可靠传输
  4. 面向字节流

UDP:

  1. 传输层协议
  2. 无连接
  3. 不可靠传输
  4. 面向数据报

3)网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分
定义网络数据流的地址:

  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  2. 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  3. 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  4. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.(0x1234abcd从0x00000000开始大端:0x1234abcd小端:0xcdab3412
  5. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  6. 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

为使网络程序具有可移植性使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
功能:

  1. htonl() 函数将无符号整数 hostlong 从主机字节顺序转换为网络字节顺序。
  2. htons() 函数将无符号短整数 hostshort 从主机字节顺序转换为网络字节顺序。
  3. ntohl() 函数将无符号整数 netlong 从网络字节顺序转换为主机字节顺序。
  4. ntohs() 函数将无符号短整数 netshort 从网络字节顺序转换为主机字节顺序。

注意:

  1. h表示host,n表示network,l表示32位长整数,s表示16位短整数
  2. 在 i386 上,主机字节顺序是最低有效字节在前,而在 Internet 上使用的网络字节顺序是最高有效字节在前
  3. 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回

4)socket编程

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6, UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同

①sockaddr结构

在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6(AF_xxx为地址族). 这样,只要取得某种sockaddr结构体的首地址,
    在这里插入图片描述
  • 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数

sockaddr结构体:

  • sa_family_t是 unsigned short即8bit (sockaddr通常作为"模板",一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in)在这里插入图片描述

sockaddr_in 结构体:

  • 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型(在sockaddr_in结构中sin_family设置为AF_INET表示IPv4) 端口号, IP地址在这里插入图片描述

in_addr结构体:

  • in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数在这里插入图片描述

sockaddr_un结构体:

  • 进程间通信的一种方式是使用UNIX套接字,人们在使用这种方式时往往用的不是网络套接字,而是一种称为本地套接字的方式。这样做可以避免为黑客留下后门在这里插入图片描述

②socket接口

这里是引用
注意:我们编写的均是用户层代码

setsockopt地址复用:server挂了立即重启:

  • int opt=1
  • setsockopt( _listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
int socket(int domain, int type, int protocol);
int close(int fd);


功能:创建socket文件描述符(TCP/UDP, 客户端 + 服务器),close()关闭socket


参数:

  1. domin:即协议家族,一般是AF_INET(表明底层使用IPV4协议)
    协议家族在<sys/socket.h>中定义
    在这里插入图片描述
  2. type:套接字种类,它指定了通信语义, 常用的是SOCK_STRAM(TCP) 和 SOCK_DGRAM(UDP)其他的有:
    在这里插入图片描述
  3. protocol:参数默认设为0(通常只存在一个协议来支持给定协议族中的特定套接字类型,指定为0)

返回值:成功时,返回新套接字的文件描述符。 出错时,返回 -1,并设置 errno

#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);


功能:绑定端口号 (TCP/UDP, 服务器)


参数:

  1. socket:socket函数返回的值,即文件描述符
  2. address:(一个结构体 struct sockaddr),实际上我们用的是sockaddr_in 并传入参数的时候强转为(struct sockaddr*)
  3. address_len:address指向的结构体大小字节数

返回值:成功0,失败-1

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);


功能:从套接字接收消息(for udp)


参数:

  1. sockfd:文件描述符
  2. buf:接收msg的缓冲区
  3. len:缓冲区大小
  4. flags:默认设置为0,阻塞方式
  5. src_addr和addrlen:输入输出型参数,获取对端的socket信息的缓冲区,长度缓冲区

返回值:成功返回收到多少字节,失败返回-1

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);


功能:发送消息到另一个套接字(for udp)


参数:

  • 同 recvfrom()

返回值:成功返回传出多少字节,失败返回-1


#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);


功能:建立监听,能否建立需要accept函数去进行检查(for tcp)


参数:

  1. sockfd:文件描述符
  2. backlog:指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求

返回值:成功返回0,失败返回-1

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);


功能:接受一个客户端的连接请求,并返回一个新的套接字(for tcp)


参数:

  1. sockfd:文件描述符
  2. addr addrlen:同bind()

返回值:成功返回一个非负整数(不同的客户端的socket对象和属于客户端的套接字),失败返回-1

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


功能:发出连接请求 (自动检查是否绑定端口,若没有绑定,则它会自动绑定一个本地端口)(for tcp)


参数:

  1. sockfd:文件描述符
  2. addr addrlen:同bind()

返回值:成功返回0,失败返回-1


③UDP示例代码及其注意点


基于网络的三子棋(MARK一下)


udp_server.cc

#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <time.h>//获取时间戳to_string((long long)time(nullptr));
#include <iostream>
#include <cstring>//bzero
#include <netinet/in.h>//sockaddr结构体成员使用时需要引入
#include <string.h> using namespace std;

void Usage(string proc)
{
        cerr<<"Usage : "<<"\n\t"<<proc<<" local_port"<<endl;
}
int main(int args,char* argv[])//命令行参数接收IP和端口 
{
        if(args != 2){
                Usage(argv[0]);
                return 1;//中断
        }
        int sock=socket(AF_INET, SOCK_DGRAM, 0);
        if(sock<0){
                cerr<<"socket error"<<endl;
                return 2;
        }
        cout<<"sock return the fd is: "<<socket<<endl;//一定为3
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));//也可使用bzero()
        local.sin_family=AF_INET;
        local.sin_port = htons(atoi(argv[1]));//注意先char转int 再 主机转网络,后续网络端口,会以源端口的方式发送给对面
        local.sin_addr.s_addr = htonl(INADDR_ANY);//主机转网络 一个IP的本质可以使用4个字节进行保存[0-255].[0-255].[0-255].[0-255],"42.165.65.134"是点分十进制字符串风格的IP
        if(bind(sock, (struct sockaddr*)&local,sizeof(local))<0){//C++中可以直接sockaddr表示结构体类型
                cerr<<"bind error"<<endl;
                return 3;
        }
        char message[1024];
        for(;;){
                memset(message, 0, sizeof(message));
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                ssize_t s=recvfrom(sock, message ,sizeof(message)-1,0,(struct sockaddr*)&peer,&len);//s是收到的内容字节长度
                if(s>0){
                        //command运行命令
                        char *command[64] ={0};
                        char *str = message;
                        command[0] = strsep(&str, " ");
                        int i=1;
                        while((command[i] = strsep(&str, " ")) && command[i]!= NULL){
                              i++;
                        }
                        command[i+1]=NULL;
                        if(fork()==0){//子进程进行程序替换
                              execvp(command[0],command);
                              cerr<<"client message# ";
                              for(int j=0;j<i;j++){
                                      printf("%s ",command[j]);
                              }
                              cout<<endl;
                              exit(4);
                        }
                        sendto(sock,command[0],strlen(command[0]),0, (struct sockaddr*)&peer,len);
                }
                else
                        //TODO
        }
        close(sock);
        return 0; 
}

server端为什么需要明确bind?
client:server = n:1, server给别人提供服务,就需要自己尽可能的将自己暴露出去(IP(域名)+PORT(一般是被隐藏的)),必须是“稳定”(不能轻易改变,尤其是端口号)的


注意:

  1. 一个IP的本质可以使用4个字节进行保存[0-255].[0-255].[0-255].[0-255], "42.165.65.134"是点分十进制字符串风格的IP
    使用库函数:int addr_t(const char*cp)可以将分十进制字符串风格的IP转为机器识别的4字节IP
  2. INADDR_ANY:云服务器在bind识,一般不能直接绑定明确的IP,直接使用INADDR_ANY,绑定所有机器上的IP(可能两张网卡),注意:不推荐直接绑定确定的ip
    在这里插入图片描述
  3. 传入参数的时候注意强转为(struct sockaddr*)
  4. memset可以替换为bzero
  5. 注意调用网络字节序和主机字节序的转换函数 htonl htons和字符点分十进制转二进制函数inet_addr()等
  6. 摘自Linux内核2.6.29,strtok函数已经不再使用,由速度更快的strsep代替
  • char *strsep(char **stringp, const char *delim);
  • stringp:注意是二级指针(), delim:分隔符
  • 模板:while((p = strsep(&str, " ")) != NULL)

udp_client.cc

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>//点分十进制转二进制
#include <arpa/inet.h>//主机转网络接口等,点分十进制转二进制 using namespace std;
void Usage(string proc) {
        cerr<<"Usage :"<<"\n\t"<<proc<< " dest_ip dest_port"<<endl;
        //return 1; } int main(int args, char* argv[]) {
        if(args != 3){//程序名 目的ip 目的端口
                Usage(argv[0]);
                return 1;
        }
        int sock=socket(AF_INET, SOCK_DGRAM, 0);
        if(sock<0){
                cerr<<"socket error"<<endl;
                return 2;
        }
        struct sockaddr_in dest;
        memset(&dest, 0, sizeof(dest));
        dest.sin_family=AF_INET;
        dest.sin_port=htons(atoi(argv[2]));//注意先htons
        dest.sin_addr.s_addr= inet_addr(argv[1]);//注意inet_addr为字符点分十进制转为二进制
        //注意client端不用绑定,而是由OS随机帮我们查找端口

        char buffer[1024];
        //模拟远程执行命令
        for(;;){
                cout<<"[RemoteTest@iZuf68hx5ixwnbhts04uheZ 4-6]# ";
                fflush(stdout);
                buffer[0]=0;
                ssize_t size=read(0, buffer, sizeof(buffer)-1);//从标准输出读数据
                if(size>0){
                        buffer[size-1]=0;
                        sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&dest, sizeof(dest));

                        struct sockaddr_in peer;
                        socklen_t len=sizeof(peer);
                        ssize_t s=recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&peer, &len);
                        if(s>0){
                                buffer[s]=0;//相当于结尾'\0'
                                cout<< "echo back from server# "<<endl;
                                cout<<buffer<<endl;
                        }
                }
        }
        close(sock);
        return 0;
}

问: client 为何不需要明确bind?
:注意,是可以绑定,如果你自己bind了,成功了还好,如果你的client端口被别的程序占用,你的client就无法启动,客户端不是必须的是哪一个端口,只需要有一个端口就可以。我们一般不自己bind,而是由OS随机帮我们查找端口

运行结果
在这里插入图片描述


更改远程执行命令并显示到客户端

popen()

注意udp不能用dup2(),udp是数据包套接字,而dup2一般是流文件,不能使用

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);


功能说明:
popen()函数通过创建一个管道, 调用fork()产生- 个子进程, 执行一个shell以运行命令来开启一个进程。可以通过这个管道执行标准输入输出操作。这个管道必须由pclose()函数关闭,必须由pclose()函数关闭,必须由pclose()函数关闭,而不是fclose()函数 (若使用fclose则会产生僵尸进程)。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shel的终止状态。如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样


command:包含 shell 的以空字符结尾的字符串的指针命令行, 该命令使用 -c flag传递给 /bin/sh,进行解释,如果有此命令,则由 shell 执行
type:同read,open等函数一样,(以只读,写…)

更改代码

更改server端程序替换部分代码 为以下代码:

if(s>0){
      FILE *in = popen(message, "r");
        if(in == nullptr)
                continue;
        string echo_message;
        char line[128];
        while(fgets(line, sizeof(line), in)){
                echo_message+=line;
        }
        sendto(sock,echo_message.c_str(),echo_message.size(),0, (struct sockaddr*)&peer,len);
}

在这里插入图片描述


④TCP示例代码及其注意点

单客户端连接

分为五个文件分别为tcp_server.hpp, tcp_client.hpp, tcp_handler.hpp, server.cc, client.cc
(hpp文件负责封装ns_tcp_server, ns_tcp_client和ns_handler类)

tcp_server.hpp

  1. 私有成员uint16_t port (端口); int listen_sock(用于获取新链接);typedef void (*handler_t)(int); (函数指针类型,用于Loop回调)
  2. InitTCPserver()初始化过程:三步:
    1.listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    2.bind(listen_sock, (struct sockaddr*)&local, sizeof(local))
    3.listen(listen_sock, backlog)注意:监听为tcp特有 ,tcp协议是面向连接的,即如果要正式传递数据之前,需要先建立链接
  3. 设计一个Loop(handler_t handler)回调机制函数,将handler进行单独封装(解耦),用回调函数调用
    1.循环sock = accept(listen_sock, (struct sockaddr*)&peer, &len)获取链接
    2.回调函数handler处理链接
    3.暂时关闭链接close(sock)

代码如下:

#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tcp_handler.hpp" 
using namespace std; 
namespace ns_tcp_server{
        typedef void (*handler_t)(int);//函数指针类型
        const int backlog=5;
        class TCPserver{
                private:
                        uint16_t port;
                        int listen_sock;//获取新链接
                public:
                        TCPserver(int _port):
                                port(_port),
                                listen_sock(-1)
                        {}
                        void InitTCPserver()
                        {
                                listen_sock = socket(AF_INET, SOCK_STREAM, 0);//1. 创建一个sock(fd文件0,1,2,3...)
                                if(listen_sock < 0){
                                        cout<<"sock error"<<endl;
                                        exit(2);
                                }

                                struct sockaddr_in local;
                                bzero(&local, sizeof(local));//初始化结构体
                                local.sin_family = AF_INET;
                                local.sin_port = htons(port);
                                local.sin_addr.s_addr = INADDR_ANY;//本机上所有任意IP
                                if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local))<0){//2. 绑定本机IP,端口等
                                                cerr<<"bind error"<<endl;
                                                exit(3);
                                }
                                //3. 监听,tcp协议是面向连接的,即如果要正式传递数据之前,需要先建立链接(udp无listen)
                                if(listen(listen_sock, backlog)<0){
                                        cerr<<"listen error"<<endl;
                                        exit(4);
                                }
                        }
                        //设计回调机制
                        void Loop(handler_t handler)
                        {
                                while(true){
                                        struct sockaddr_in peer;
                                        socklen_t len = sizeof(peer);
                                        //获取链接,注意tcp没有rcvfrom和sendto 而是使用accept
                                        int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
                                        if(sock<0){
                                                cout<<"accept error"<<endl;
                                                continue;
                                        }
                                        //打印sock fd值,端口号和ip地址
                                        cout<<"the accept sock_fd is"<<sock<<endl;
                                        uint16_t peer_port = ntohs(peer.sin_port);
                                        string peer_ip = inet_ntoa(peer.sin_addr);//转点分十进制字符串
                                        cout<<"connect IP: "<<peer_ip<<" Port: "<<peer_port<<endl;
                                        //处理链接 回调
                                        handler(sock);
                                        close(sock);
                                }
                        }
                        ~TCPserver()
                        {
                                if(listen_sock>=0) close(listen_sock);//创建成功才能关闭
                        }
        }; 
}; 

tcp_handler.hpp

  1. while循环不断从sock套接字中读取,处理,写回sock,(接收到”quit“就break,即断开与client的链接)

代码如下

#pragma once
#include "tcp_server.hpp"
#include <unistd.h>
#include <unordered_map>
#include <sys/wait.h>
#include <pthread.h> 
using namespace std; 
namespace ns_handler{
        //using namespace ns_tcp_server;
        void HandlerHelper(int sock)
        {
                while(true){
                        char buffer[1024];
                        ssize_t s= read(sock, buffer, sizeof(buffer)-1);
                        if(s>0){//read success
                                buffer[s]=0;
                                cout<<" client# "<<buffer<<endl;
                                string echo_string=buffer;
                                if(echo_string == "quit")
                                        break;
                                echo_string+="[server echo]";
                                write(sock, echo_string.c_str(), echo_string.size());
                        }
                        else if(s==0)//对端链接关闭
                        else//读取失败
                }
        } 
};

tcp_client.hpp

  1. 私有成员string desc_ip; (client要访问的对端服务器的IP地址)uint16_t desc_port; (client要访问的对端服务器的port端口号)int sock(tcp是流式文件);
  2. InitTCPclient()只需要创建TCP套接字sock = socket(AF_INET, SOCK_STREAM, 0);不需要绑定(bind),监听(listen),接收(accept)
  3. tcp是面向连接的,client 要通信之前必须先连接,封装一个Start()函数
    1.填充对方服务器的socket信息: struct sockaddr_in svr; svr.sin_family=...
    2.发起链接请求connect(sock, (struct sockaddr*)&svr, sizeof(svr))
    3.完成业务逻辑: while循环不断从标准输入中读取(注意先fflush(stdout)),写入到sock,经过tcp_server处理,再次从sock读出并打印

代码如下:

#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h> 
using namespace std; 
namespace ns_tcp_client{
        class TCPclient{
                private:
                        string dest_ip;//对端服务器ip
                        uint16_t dest_port;//对端服务器port
                        int sock;//fd
                public:
                        TCPclient(string _ip, uint16_t _port)
                                :dest_ip(_ip)
                                 ,dest_port(_port)
                                 ,sock(-1)
                        {}
                        void InitTCPclient()
                        {
                                //创建socket
                                sock = socket(AF_INET, SOCK_STREAM, 0);
                                if(sock<0){
                                        cerr<<"socket error"<<endl;
                                        exit(1);
                                }
                                //注意:tcp_client不用绑定bind,监听listen,接收accept
                        }
                        //tcp面向连接,client要通信前要先连接
                        void Start()
                        {
                                //填充对端服务器socket信息
                                struct sockaddr_in svr;
                                bzero(&svr, sizeof(svr));
                                svr.sin_family=AF_INET;
                                svr.sin_port = htons(dest_port);
                                svr.sin_addr.s_addr = inet_addr(dest_ip.c_str());//注意用.c_str()将string转c字符
                                //发起链接请求
                                if(connect(sock, (struct sockaddr*)&svr, sizeof(svr))==0)//connect成功返回0
                                        cout<<"connection succeed"<<endl;
                                else{
                                        cout<<"connection failed"<<endl;
                                        return;
                                }
                                while(true){
                                        char buffer[1024] = {0};
                                        cout<<"Please Enter:>";
                                        fflush(stdout);//需要刷新一下标准输出
                                        ssize_t s = read(0, buffer, sizeof(buffer-1));//从标准输入读取
                                        if(s>0){
                                                buffer[s-1]=0;//去除回车
                                                write(sock, buffer, strlen(buffer));//写入sock文件描述符
                                                //再从sock里读出来,相当于服务器回显的
                                                ssize_t rs = read(sock, buffer, sizeof(buffer)-1);
                                                if(rs>0){
                                                        buffer[rs]=0;//标记'\0'
                                                        cout<<buffer<<endl;
                                                }
                                                else{
                                                        cout<<"server has cut the link with u, cannot rcv the echo message"<<endl;
                                                        break;
                                                }

                                        }
                                }
                        }
                        ~TCPclient()
                        {
                                if(sock >= 0) close(sock);
                        }
        }; 
};

server.ccclient.cc调用即可
代码如下:

#include "tcp_server.hpp" //提供网络连接功能
#include "tcp_handler.hpp" //提供网络sock的处理功能
static void Usage(std::string proc)
{
    std::cerr << "Usage:" << "\n\t" << proc << " port" << std::endl;  } int main(int argc, char *argv[]) {
    if(argc != 2){//命令行参数的验证
        Usage(argv[0]);
        return 0;
    }
    uint16_t port = atoi(argv[1]);
    ns_tcpserver::TcpServer *svr = new ns_tcpserver::TcpServer(port);
    svr->InitTcpServer();
    svr->Loop(ns_handler::HandlerHelper);
    return 0; 
}
#include "tcp_client.hpp" 
static void Usage(std::string proc) 
{
    std::cerr << "Usage: " << "\n\t" << proc << " svr_ip svr_port" << std::endl; 
} 
// ./tcp_client peer_ip peer_port 
int main(int argc, char*argv[]) 
{
    if(argc != 3){
        Usage(argv[0]);
        return 1;
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    ns_tcpclient::TcpClient cli(ip, port);
    cli.InitTcpClient();
    cli.Start();
    return 0; 
}

在这里插入图片描述
可以看到只支持单客户端,一旦我们退出第一个客户端,马上就会回显出第二个客户端的内容
在这里插入图片描述

多进程实现多客户端连接 (优点:稳定)

只需要更改HandlerHelper方法


法一:封装HandlerSock_2来创建子进程处理HandlerHelper业务,同时再tcp_server.hpp的Loop函数中加入signal(SIGCHLD, SIG_IGN);不等待子进程返回

Loop(handler_t handler) 
{ 	
	signal(SIGCHLD, SIG_IGN); 	
	 //... 
}
void HandlerSock_2(int sock) 
{
    //多进程
    if(fork() == 0){  //child
        HandlerHelper(sock);
        exit(0);
    }
    //father
    //close(sock);
    //waitpid(-1, nullptr, 0); 
} 

法二 技巧:创建孙子进程,让孙子进程执行HandlerHelper方法,同时让子进程退出,孙子进程变为孤儿进程,被OS领养,父进程waitpid接收到子进程退出的信息,继续向下执行

void HandlerSock_V2(int sock) 
{
    //多进程
    if(fork() == 0){ //child
        if(fork() > 0){//child
            exit(0);
        }     
        HandlerHelper(sock);//grandchild,孤儿进程,会被OS领养
        exit(0);
    }
    //father
    //close(sock);
    waitpid(-1, nullptr, 0); 
    close(sock)
} 

在这里插入图片描述

多线程实现多客户端连接 (优点:轻量化 缺点:健壮性差)

多进程开销太大,改为多线程

Loop(handler_t handler) 
{ 	
	 	  //close(sock);
}
void *thread_routine(void *args)
{
          int sock = *(int*)args;
          delete (int*)args;//释放在堆上开辟的空间
          std::cout << "debug: " << sock << std::endl;
          pthread_detach(pthread_self());
          HandlerHelper(sock); //业务逻辑
          //已经处理完毕,需要关闭不需要的fd,如果不关闭,就造成了文件描述符泄漏!
          close(sock);
          return nullptr;  
}
void HandlerSock_V3(int sock)  
{
         //多线程版本
         pthread_t tid;
         int *p = new int(sock);//子线程会detach分离,
         //如果直接传sock的地址(sock是一个局部变量,thread_routine使用的时候可能已经销毁,所以在堆上开辟一个空间存sock的值)
         pthread_create(&tid, nullptr, thread_routine, p);  
}

注意:1. 如果直接传sock的地址(sock是一个局部变量,thread_routine使用的时候可能已经销毁,所以在堆上开辟一个空间存sock的值)
注意:2. 多线程可能会出现还没有进行处理,Loop中的close(sock)就把文件关闭了的问题
注意:3. 由于Loop函数体没有执行close(sock),每次的fd文件描述符都不同,这样会导致文件描述符泄漏问题,所有客户端退出后下一次再连接会从上一次结束时的fd向上递增,所以我们要在HandlerHelper(sock)处理完毕后关闭不需要的文件描述符
在这里插入图片描述

线程池实现实现多客户端连接 ()

前面实现的方法优缺点:

  1. 单进程不用
  2. 多进程:链接来了,才创建进程,而且没有上限, 优点: 稳定,进程是具独立性的 缺点:资源占用太大
  3. 多线程:链接来了,才创建线程,而且没有上限, 优点: 轻量化,缺点:健壮性不足

一旦系统中的进程或者线程极度增多,进程或者线程在系统内切换的成本增加,切换的周期变长了,甚至导致系统宕机


线程池代码见:Linux----多线程(下)
在第一次调用单例顺便一起初始化
thread_pool.hpp

static ThreadPool<T> *get_instance() 
{
        static pthread_mutex_t mtx= PTHREAD_MUTEX_INITIALIZER;//不需要destroy销毁mtx
        if(nullptr == instance){
                pthread_mutex_lock(&mtx);
                if(nullptr==instance){
                        instance=new ThreadPool<T>();
                        //instance->InitThreadPool(num);//第一次调用单例顺便一起初始化
                }
                pthread_mutex_unlock(&mtx);
        }
        return instance; 
} 

tcp_handler.hpp

class task{
        private:
                int sock;
        public:
                task(){}
                task(int _sock):sock(_sock){}
                void operator()()//线程池里会在Routine函数体内执行t()执行任务,即重载(),调用HandlerHelper(sock)
                {
                        HandlerHelper(sock);
                        close(sock);
                }
                ~task(){} }; void HandlerSock_4(int sock) {
        ThreadPool<task> *tp=ThreadPool<task>::get_instance(5);
        tp->InitThreadPool(5);
        tp->PushTask(task(sock)); 
} 

在这里插入图片描述

简易server翻译实现(基于线程池)

更改回调函数HandlerHelper()即可

std::unordered_map<std::string, std::string> dict = {
    {"is" , "是"},
    {"hello", "你好"},
    {"this", "这个"},
    {"that", "那个"}, 
}; 
void HandlerHelper(int sock) {
    char buffer[1024];
    ssize_t s = read(sock, buffer, sizeof(buffer)-1);
    if(s>0){
        buffer[s] = 0;
        std::cout << "client say# " << buffer <<std::endl;
        std::string k = buffer;
        std::string  message = "字典无此单词";
        auto iter = dict.find(k);
        if(iter != dict.end()){
            message = iter->second;
     }
     write(sock, message.c_str(), message.size());
     std::cout << "server echo# " << message <<std::endl;  
} 

在这里插入图片描述

5)TCP协议通信流程概览

TCP是面向连接的
连接的本质是连上了server的OS(协议栈)模块
服务:客户=1:n,所以OS要管理这些连接->先描述再组织

  • 描述组织:OS内维护对应的连接数据结构

建立连接的本质:在双方OS内维护对应的连接数据结构(需要空间+时间)


CS双方OS自动进行三次握手(一方主动另一方被动)连接,四次挥手(释放双方建立好的数据结构,断开连接需要双方同意)断开在这里插入图片描述
服务器初始化:

  1. 调用socket, 创建文件描述符;
  2. 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  3. 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  4. 调用accecpt, 并阻塞, 等待客户端连接过来

建立连接的过程(这个建立连接的过程, 通常称为 三次握手;):

  1. 调用socket, 创建文件描述符;
  2. 调用connect, 向服务器发起连接请求;
  3. connect会发出SYN段并阻塞等待服务器应答; (第一次)
  4. 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  5. 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

数据传输过程:

  1. 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据例如管道;
  2. 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
  3. 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
  4. 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
  5. 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程(这个断开连接的过程, 通常称为 四次挥手):

  1. 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
  2. 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
  3. read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
  4. 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

6)TCP对比UDP

表格对比:

TCPUDP
可靠传输不可靠传输
有连接无连接
字节流数据报
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值