Linux网络编程套接字(上)

news2025/2/12 7:37:59

目录

预备知识 

理解源IP地址和目的IP地址 :

认识端口号:

理解"端口号"和"进程ID"

认识TCP/UDP协议 

TCP: 

UDP :

网络字节序

Socket编程接口

Socket常见API:

Sockaddr结构:

简单的UDP网络程序 

 实现一个简单的收发功能:

封装一下UdpSocket :

 server端:

client端:

makefile:

​编辑 

 地址转换函数

 关于inet_ntoa

 简单的TCP网络程序

TCP socket API 详解:

socket():

bind():

listen():

accept():

connet():

实现一个简单的收发功能:

 封装一下TcpSocket:

server端:

client端: 

 makefile:

多进程TCP服务器 

 多线程TCP服务器

TCP协议通讯流程


 

预备知识 

理解源IP地址和目的IP地址 :

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。

  • 源IP地址(Source IP Address)是指发送数据包的主机或设备的IP地址。它表示数据包的起始点或来源。源IP地址可以告诉目的主机或路由器哪个主机或设备发送了该数据包。
  • 目的IP地址(Destination IP Address)是指接收数据包的主机或设备的IP地址。它表示数据包的结束点或目标。目的IP地址告诉网络设备或路由器将数据包传递给哪个特定的主机或设备。

认识端口号:

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

理解"端口号"和"进程ID"

进程ID也是标识主机上的唯一进程,而端口号也是如此,那为什么端口号不直接使用进程ID呢?

  • 端口号具有标准化:为了确保在多个网络设备和操作系统之间的互操作性,已经定义了许多标准的端口号。例如,HTTP通常使用80端口,HTTPS使用443端口等,端口号自成一派,不希望与进程ID有任何耦合
  • 动态分配和管理:使用进程ID作为端口号可能会导致一些问题。进程ID是由操作系统分配和管理的,通常不会在不同的主机之间保持一致。此外,进程ID可能因为进程的启动、重启或关闭而发生变化而端口号通常是应用程序在运行时通过系统调用动态分配的并且具有固定的范围(例如0-65535)。这样可以确保端口号的唯一性一致性,使网络设备能够准确识别和路由数据包。
  • 安全性和隔离性:使用端口号可以提供更好的安全性和隔离性。操作系统可以通过控制端口的访问权限和网络流量,确保只有经过授权的应用程序能够监听和使用特定的端口。这样可以减少潜在的安全威胁和攻击。

更加详细的预备知识:

Linux网络基础https://mp.csdn.net/mp_blog/creation/editor/132146472


认识TCP/UDP协议 

TCP: 

  • 是传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

UDP :

  • 是传输层协议
  • 无连接
  • 不可靠
  • 面向数据报 

网络字节序

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

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

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

  •  h表示host,n表示network,l表示32长整数,s表示16位短整数
  • 例:htonl表示将32位的长整数从数据字节序转化为网络字节序,例如将IP地址转换后准备发送
  • 如果主机是小端字节序,这些函数将参数做相应的大小转换后返回
  • 如果主机是大端字节序,这些函数不做转化,将参数原封不动返回

Socket编程接口

Socket常见API:

//创建 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);

Sockaddr结构:

socket API是一层抽象的网络编程接口,适用于各种底层网络ipv4、ipv6以及UNIX Domain Socket

然而各种网络协议的地址格式并不相同.

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

简单的UDP网络程序 

 实现一个简单的收发功能:

封装一下UdpSocket :

#pragma once
#include<string>
#include<stdlib.h>
#include<unistd.h>
#include<cstdio>
#include<memory>


#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
class UdpSocket
{
public:
    UdpSocket():fd_(-1)
    {}
    bool Socket()//创建Socket,这一步也可以放构造函数里面
    {
        fd_ = socket(AF_INET,SOCK_DGRAM,0);
        if(fd_<0)
        {
            perror("socket");
            return false;
        }
        return true;
    }
    bool Bind(const std::string& ip,uint16_t port)//将IP和端口绑定起来
    {
        struct sockaddr_in addr;//ipv4所以用_in
        addr.sin_family = AF_INET;//表示一下当前的地址族,这里表示ipv4
        addr.sin_addr.s_addr = inet_addr(ip.c_str());//对当前的对象addr绑定IP
        addr.sin_port = htons(port);//注意大小端所以在这里转化
        int isBind = bind(fd_,(sockaddr*)&addr,sizeof(addr));
        if(!isBind) {
            perror("bind");
            return false;
        }
        return true;
    }
    bool RecvFrom(std::string* buf,std::string* ip = NULL,uint16_t* port=NULL)
    {
        char tmp[1024]={0};//一个接收缓冲区
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t read_size = recvfrom(fd_,tmp,sizeof(tmp)-1,0,(sockaddr*)&peer,&len);
        if(read_size < 0)
        {
            perror("recvfrom");
            return false;
        }
        buf->assign(tmp,read_size);//将读到的缓冲区放到输入参数中
        if(ip != NULL)
        {
            *ip = inet_ntoa(peer.sin_addr);
        }
        if(port != NULL)
        {
            *port = ntohs(peer.sin_port);
        }
        return true;
    }
    bool SendTo(const std::string& buf,const std::string& ip,uint16_t port)
    {
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        ssize_t write_size = sendto(fd_,buf.data(),buf.size(),0,(sockaddr*)&addr,sizeof(addr));
        if(write_size < 0)
        {
            perror("sendto");
            return false;
        }

        return true;
    }
    bool Close()//要记得关闭
    {
        close(fd_);
        return true;
    }
private:
    int fd_;
};

 server端:

//"udp_server.hpp"
#include"udp_socket.hpp"
#include<functional>
#include<cassert>
#include<memory>
#include<iostream>
typedef std::function<void(const std::string&req,std::string* resp)> Handler;

class UdpServer
{
public:
    UdpServer(){
        assert(sock_.Socket());
    }
    ~UdpServer(){
        sock_.Close();
    }
    bool Start(const std::string& ip,uint16_t port,Handler Handler)
    {
        //1.创建 socket
        //2.绑定 端口号
        bool ret = sock_.Bind(ip,port);
        if(!ret) return false;
        //服务器开启后是一个死循环
        while(true)
        {  
            std::cout<<"服务器启动"<<std::endl;
            //尝试读取请求
            std::string req;
            std::string remote_ip;
            uint16_t remote_port = 0;
            bool ret = sock_.RecvFrom(&req,&remote_ip,&remote_port);
            if(!ret)
            {
                continue;
            }

            std::string resp;
            //根据请求计算相应
            Handler(req,&resp);
            //返回响应给客户端
            sock_.SendTo(resp,remote_ip,remote_port);
            printf("[%s:%d] req: %s , resp: %s\n",remote_ip.c_str(),remote_port,req.c_str(),resp.c_str());
        }
        sock_.Close();
        return true;
    }
private:
    UdpSocket sock_;
};
void MyTestHandler(const std::string& req,std::string* resp)
{
    //req = requese resp = response
    //这里直接输入啥返回啥
    //如果上层想改变底层的方法直接改Handler就可以了
    *resp = req.c_str();
}
//"udp_server.cc"
#include"udp_server.hpp"
#include<iostream>
#include<functional>

int main(int argc,char* argv[])
{
    if(argc!=3){
        printf("Usage ./udp_server [ip] [port]\n");
        return 1;
    }
    UdpServer server;
    while(1) server.Start(argv[1],atoi(argv[2]),MyTestHandler);
    
    return 0;
}

client端:

//"udp_client.hpp"
#include "udp_socket.hpp"
#include <cassert>
#include <memory>
#include <iostream>
class UdpClient
{
public:
    UdpClient(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
    {
        assert(sock_.Socket());
    }
    ~UdpClient()
    {
        sock_.Close();
    }
    bool RecvFrom(std::string *buf)
    {
        return sock_.RecvFrom(buf);
    }
    bool SendTo(const std::string &buf)
    {
        return sock_.SendTo(buf, ip_, port_);
    }

private:
    UdpSocket sock_;
    // 服务器端的 IP 和 端口号
    std::string ip_;
    uint16_t port_;
};

//"udp_client.cc"
#include"udp_client.hpp"
int main(int argc,char* argv[])
{
    if(argc!=3){
        printf("Usage ./udp_client [ip] [port]\n");
        return 1;
    }
    //UdpClient client();
    std::unique_ptr<UdpClient> client(new UdpClient(argv[1],atoi(argv[2])));
    for(;;)
    {
        std::string input;
        std::cout<<"来随便输入一点东西:>";
        std::cin>> input;
        if(!std::cin){
            std::cout<<"seeya"<<std::endl;
            break;
        }
        client->SendTo(input);
        std::string result;
        client->RecvFrom(&result);
        std::cout<<"服务端给了你这么个玩意:>"<< result <<std::endl;
    }
    return 0;
}

makefile:

.PHONY:all
all:server client
server:udp_server.cc
	g++ -o $@ $^ -std=c++11
client:udp_client.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f server client

 


 地址转换函数

本章值接收基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr.sin_addr表示32位的IP地址,但是我们通常使用的是点分十进制字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串转in_addr的函数:

#include<arpa/inet.h>
int inet_aton(const char* strptr,struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family,const char* strptr,void* addrptr);

in_addr转字符串的函数: 

char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family,const void *addrptr,char* strptr,size_t len);

其中inet_pton 和int_ntop不仅可以转化IPv4的in_addr,还可以转化IPv6的in6_addr,因此接口函数是void* addrptr; 

示例:

#include<cstdio>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main()
{
    struct sockaddr_in addr;
    inet_aton("127.0.0.1", &addr.sin_addr);
    uint32_t* ptr = (uint32_t*)(&addr.sin_addr);
    printf("addr:%x\n",*ptr);
    printf("addr_str:%s\n",inet_ntoa(addr.sin_addr));
    return 0;
}
//输出结果
//addr:100007f
//addr_str:127.0.0.1

 关于inet_ntoa

inet_ntoa这个函数返回了一个char* ,很显然是这个函数在机子内部为我们申请了一块空间来保存ip的结果,那么是否需要调用者手动释放呢?

 man手册上说,inet_ntoa函数,是把这个返回值放到了静态存储区,这时不需要手动进行释放

但如果我多次调用会发生什么?

#include<cstdio>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    char* ptr1 = inet_ntoa(addr1.sin_addr);
    char* ptr2 = inet_ntoa(addr2.sin_addr);
    printf("ptr1:%s,ptr2:%s\n",ptr1,ptr2);
    return 0;
}
//输出结果
//ptr1:255.255.255.255, ptr2:255.255.255.255

因为inet_ntoa把结果放到自己内部 的一个静态存储区,这样二次调用就会覆盖上一次的结果

  • 在APUE中,明确提出inet_ntoa不是线程安全的函数;
  • 多线程环境下推荐使用,inet_ntop这个函数由调用者提供一个缓冲区保存结果,可以由程序员操作一下来规避线程安全问题(lock);

 简单的TCP网络程序

TCP socket API 详解:

下面所用的socket API这些函数都在sys/socket.h中

socket():

#include<sys/types.h>
#include<sys/socket.h>

int socket(int domain,int type,int protocol);
  • socket()打开一个网络通讯端口,如果成功的话就像open()一样返回一个文件描述符
  • 应用程序可以像读文件一样用read/write在网络上收发数据 
  • 如果socket()调用储存则返回-1
  • 对于IPv4,family参数指定为AF_INET;
  • 对于TCP协议,type的参数指定为SOCK_STREAM,表示面向流的传输协议

bind():

#include<sys/types.h>
#include<sys/socket.h>

int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
  •  服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号;
  • bind()成功返回0,失败返回-1
  • bind()作用是将参数sockfd和myaddr绑定在一起,使用sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
  • struct sockaddr* 是一个通用指针类型,myaddr参数实际上可以 接收多种协议的sockaddr结构体,而他们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度

程序中可以这么对myaddr的参数进行初始化

bzero(&servadddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htol(INADDR_ANY);
servaddr.sin_port = htos(SERV_PORT);
  1. 将整个结构体清零
  2. 设置地址类型为AF_INET (设置地址族)
  3. 网络定义为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样可以设置在所有的IP上监听,直到有客户端建立连接后才确定具体IP地址
  4. 端口号为SERV_PORT

listen():

#include<sys/types.h>
#include<sys/socket.h>

int listen(int sockfd,int backlog);
  • listen()声明sockfd处于监听状态,并且允许有最多backlog个客户端进行连接等待状态,如果接受到更多请求就忽略
  • listen()成功返回0,失败返回-1

accept():

#include<sys/types.h>
#include<sys/socket.h>

int accept(int sockfd,struct sockaddr *addr,socklen_t* addrlen);
  • 三次握手完成后,服务器调用accept()接收连接;
  • 如果服务器调用accept()时还没有客户端连接的话,就阻塞式等待 直到有客户端连接上
  • addr是一个输出型参数,accept返回的时候,会传出客户端的地址和端口号
  • 如果给addr参数传NULL,表示不关心客户端的地址
  • addrlen参数是一个输入输出型参数,传入的是调用者提供的,缓冲区addr的长度,以避免缓冲区溢出问题,传出的时客户端地址结构体的实际长度

accept的返回值:

  • 如果成功接受连接请求并创建新的连接套接字,则返回一个非负整数,表示新的连接套接字描述符。这个描述符可以用于后续的数据传输操作 
  • 如果发生错误,accept() 返回 -1,并设置合适的错误码

常见的accept的错误码:(通过errno全局变量访问)。常见的错误码包括EAGAIN(指示套接字非阻塞且没有等待连接)、EINTR指示调用被信号中断)、EINVAL(指示套接字无效)等。 

connet():

#include<sys/types.h>
#include<sys/socket.h>

int connet(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  • 客户端需要调用connect()连接服务器
  • connect和bind的参数形式一致,区别在意bind的参数是自己的地址,connect参数是对方的地址
  • 成功返回0失败返回-1  

实现一个简单的收发功能

 封装一下TcpSocket:

//"tcp_socket.hpp"
#pragma
#include<cstdio>
#include<string>
#include<unistd.h>
#include<assert.h>
#include<functional>

#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>

class TcpSocket
{
public:
    TcpSocket():fd_(-1){}
    TcpSocket(int fd):fd_(fd){}

    bool Socket()
    {
        fd_ = socket(AF_INET,SOCK_STREAM,0);//注意这里是SOCK_STREAM跟udp不一样
        if(fd_<0){
            perror("socket");
            return false;
        }
        printf("opened fd = %d\n",fd_);
        return true;
    }
    bool Close(){ 
        printf("close fd = %d",fd_);
        close(fd_);
        return true;
    }
    bool Bind(const std::string& ip,uint16_t port){
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;//绑定协议族一样是AF_INET(ipv4)
        addr.sin_addr.s_addr = inet_addr(ip.c_str());//这里IP也可以绑定成INADDR_ANY
        addr.sin_port = htons(port);
        int ret = bind(fd_,(struct sockaddr*)&addr,sizeof(addr));
        if(ret<0){
            perror("bind"); 
            return false;
        } 
        return true; 
    }
    bool Listen(int num){
        int ret = listen(fd_,num);
        if(ret<0){
            perror("listen");
            return false;
        }
        return true;
    }
    bool Accept(TcpSocket* peer,std::string* ip,uint16_t* port = NULL)
    {
        struct sockaddr_in peer_addr;
        socklen_t len = sizeof(peer_addr);
        int new_sock = accept(fd_,(sockaddr*)&peer_addr,&len);//服务端进去客户端出来
        if(new_sock < 0){
            perror("accept");
            return false;
        }
        printf("accept fd = %d\n",new_sock);
        peer->fd_ = new_sock;
        if(ip!=NULL){
            *ip = inet_ntoa(peer_addr.sin_addr);
        }
        if(port!=NULL){
            *port = ntohs(peer_addr.sin_port);
        }
        return true;
    }

    bool Recv(std::string* buf){
        buf->clear();
        char tmp[1024]={0};
        ssize_t read_size = recv(fd_,tmp,sizeof(tmp),0);
        if(read_size<0){
            perror("recv");
            return false;
        }
        if(read_size==0){
            return false;
        }
        buf->assign(tmp,read_size);
        return true;
    }
    bool Send(const std::string& buf){
        ssize_t write_size = send(fd_,buf.data(),buf.size(),0);
        if(write_size < 0){
            perror("send");
            return false;
        }
        return true;
    }
    bool Connect(const std::string& ip,uint16_t port)
    {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        int ret = connect(fd_,(struct sockaddr*)&addr,sizeof(addr));
        if(ret<0){
            perror("connect");
            return false;
        }
        return true;
    }
public:
    int fd_;
};

server端:

//"tcp_server.hpp"
#include"tcp_socket.hpp"
typedef std::function<void(const std::string&req , std::string* resp)> Handler;
class TcpServer
{
public:
    TcpServer(){}
    TcpServer(std::string ip,uint64_t port)
    :ip_(ip),port_(port)
    {}
    bool Start(Handler handler)
    {
        //创建socket   
        listen_sock_.Socket();
        //绑定端口号
        listen_sock_.Bind(ip_,port_);
        //进行监听
        listen_sock_.Listen(10);
        //进入事件
        for(;;)
        {
            TcpSocket new_sock;//客户端
            std::string ip;
            uint16_t port = 0;
            if(!listen_sock_.Accept(&new_sock,&ip,&port))//在我accept成功后获取底层链接就成功了,之后用新的new_sock和client的ip、端口进行通信
            {
                continue;//这里的ip和port都改成client端的,而new_sock也是连接成功后新生成的
            }
            //所以这时候new_sock里面的_fd就是新的fd,使用new_sock就可以进行通信
            printf("[client %s:%d] connect!\n",ip.c_str(),port);
            while(true)
            {
                std::string req;
                bool ret = new_sock.Recv(&req);
                if(!ret){
                    printf("[client %s:%d] disconnect!\n",ip.c_str(),port);
                    new_sock.Close();
                    break;
                }
                //处理接收的req
                std::string resp;
                handler(req,&resp);

                new_sock.Send(resp);
                printf("[%s:%d]req:%s,resp:%s\n",ip.c_str(),port,req.c_str(),resp.c_str());
            }
        }
        return true;
    }
private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint16_t port_;
};
//"tcp_server"
#include"tcp_server.hpp"
#include<memory>
void MyTestHandler(const std::string& req,std::string* resp)
{
    //这里实现服务器方法
    *resp = req;
}
int main(int argc,char* argv[])
{
    if(argc!=3){
        exit(-1);
    }
    std::unique_ptr<TcpServer> ser(new TcpServer(argv[1],atoi(argv[2])));
    ser->Start(MyTestHandler);
    return 0;
}

client端: 

//"tcp_client.hpp"
#include"tcp_socket.hpp"
class TcpClient
{
public:
    TcpClient(const std::string& ip,uint16_t port)
    :ip_(ip)
    ,port_(port)
    {
        sock_.Socket();
    }
    ~TcpClient()
    {
        sock_.Close();
    }
    bool Connect()
    {
        return sock_.Connect(ip_,port_);
    }
    bool Recv(std::string* buf)
    {
        return sock_.Recv(buf);
    }
    bool Send(const std::string& buf)
    {
        return sock_.Send(buf);
    }
private:
    TcpSocket sock_;
    std::string ip_;
    uint16_t port_;
};
//"tcp_client.cc"
#include"tcp_client.hpp"
#include<memory>
#include<iostream>
int main(int argc,char* argv[])
{
    std::unique_ptr<TcpClient> client(new TcpClient(argv[1],atoi(argv[2])));
    bool ret = client->Connect();
    if(!ret) return 1;
    for(;;)
    {
        std::cout<<"输入向服务器发出的数据:>";
        std::string input;
        std::cin >> input;
        if(!std::cin){
            break;
        }
        client->Send(input);
        std::string res;
        client->Recv(&res);
        std::cout<< res << std::endl;    
    }
    return 0;
}

 makefile:

.PHONY:all
all:server client
server:tcp_server.cc
	g++ -o $@ $^ -std=c++11
client:tcp_client.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f server client

注意:

  •  客户端不需要固定的端口号,所以不必调用bind,客户端的端口号由内核自动分配
  • 客户端调用不是不允许使用bind,只是没有必要bind一个固定端口后,否则在同一台机器上启动多个客户端就会出现端口号被占用,而无法正确的建立连接
  • 同样服务端可不是必须调用bind,但是如果服务器不调用bind,内核会给服务器自动分配监听端口,每次启动服务器时的端口号都不一样客户端要连接服务器就会有麻烦
  • 同样的上面的服务器只能单个客户端进行访问,原因是因为accept一个请求之后,就一直在while里面循环尝试read没有继续accept,导致不能接受新的请求

多进程TCP服务器 

上面所写的程序只能单个客户端连接,这显然是不符合常理的,所以我们可以用多进程的方法:

 TcpProcessServer:

//"TcpProcessServer"
#pragma once
#include <functional>
#include <signal.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string &req, std::string *resp)> Handler;
// 多进程版本的 Tcp 服务器
class TcpProcessServer
{
public:
    TcpProcessServer(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
    {
        // 需要处理子进程
        signal(SIGCHLD, SIG_IGN);
    }
    void ProcessConnect(TcpSocket &new_sock, const std::string &ip, uint16_t port, Handler handler)
    {
        int ret = fork();
        if (ret > 0)
        {
            // father
            // 父进程不需要做额外的操作, 直接返回即可.
            // 思考, 这里能否使用 wait 进行进程等待?
            // 如果使用 wait , 会导致父进程不能快速再次调用到 accept, 仍然没法处理多个请求
            // [注意!!] 父进程需要关闭 new_sock
            new_sock.Close();
            return;
        }
        else if (ret == 0)
        {
            // child
            // 处理具体的连接过程. 每个连接一个子进程
            for (;;)
            {
                std::string req;
                bool ret = new_sock.Recv(&req);
                if (!ret)
                {
                    // 当前的请求处理完了, 可以退出子进程了. 注意, socket 的关闭在析构函数中就完成了
                    printf("[client %s:%d] disconnected!\n", ip.c_str(), port);
                    exit(0);
                }
                std::string resp;
                handler(req, &resp);
                new_sock.Send(resp);
                printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port,
                       req.c_str(), resp.c_str());
            }
        }
        else
        {
            perror("fork");
        }
    }
    bool Start(Handler handler)
    {
        // 1. 创建 socket;
        listen_sock_.Socket();
        // 2. 绑定端口号
        listen_sock_.Bind(ip_, port_);
        // 3. 进行监听
        listen_sock_.Listen(5);
        // 4. 进入事件循环
        for (;;)
        {
            // 5. 进行 accept
            TcpSocket new_sock;
            std::string ip;
            uint16_t port = 0;
            if (!listen_sock_.Accept(&new_sock, &ip, &port))
            {
                continue;
            }
            printf("[client %s:%d] connect!\n", ip.c_str(), port);
            ProcessConnect(new_sock, ip, port, handler);
        }
        return true;
    }
    private:
        TcpSocket listen_sock_;
        std::string ip_;
        uint64_t port_;
    
};

 多线程TCP服务器

 TcpPhtreadServer.hpp

//"TcpPthreadServer.hpp"
#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string &, std::string *)> Handler;
struct ThreadArg
{
    TcpSocket new_sock;
    std::string ip;
    uint16_t port;
    Handler handler;
};
class TcpThreadServer
{
public:
    TcpThreadServer(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
    {
    }
    bool Start(Handler handler)
    {
        // 1. 创建 socket;
        listen_sock_.Socket();
        // 2. 绑定端口号
        listen_sock_.Bind(ip_, port_);
        // 3. 进行监听
        listen_sock_.Listen(5);
        // 4. 进入循环
        for (;;)
        {
            // 5. 进行 accept
            ThreadArg *arg = new ThreadArg();
            arg->handler = handler;
            bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);
            if (!ret)
            {
                continue;
            }
            printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);
            // 6. 创建新的线程完成具体操作
            pthread_t tid;
            pthread_create(&tid, NULL, ThreadEntry, arg);
            pthread_detach(tid);
        }
        return true;
    }
    // 这里的成员函数为啥非得是 static?
    static void *ThreadEntry(void *arg)
    {
        // C++ 的四种类型转换都是什么?
        ThreadArg *p = reinterpret_cast<ThreadArg *>(arg);
        ProcessConnect(p);
        // 一定要记得释放内存!!! 也要记得关闭文件描述符
        p->new_sock.Close();
        delete p;
        return NULL;
    }
    // 处理单次连接. 这个函数也得是 static
    static void ProcessConnect(ThreadArg *arg)
    {
        // 1. 循环进行读写
        for (;;)
        {
            std::string req;
            // 2. 读取请求
            bool ret = arg->new_sock.Recv(&req);
            if (!ret)
            {
                printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);
                break;
            }
            std::string resp;
            // 3. 根据请求计算响应
            arg->handler(req, &resp);
            // 4. 发送响应
            arg->new_sock.Send(resp);
            printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(),
                   arg->port, req.c_str(), resp.c_str());
        }
    }

private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint16_t port_;
};

TCP协议通讯流程

下面是基于TCP协议的客户端/服务器程序的一般流程: 

 

 服务器初始化:

  • 调用socket,创建文件描述符
  • 调用bind,将当前的文件描述符和ip/port绑定在一起,如果端口被占用的话就会绑定失败
  • 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备
  • accept并阻塞等待客户端连接过来

建立连接的过程:

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

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

数据传输的过程:

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

断开连接的过程:

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

这个断开的过程中通常称为四次挥手

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/864189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全面讲解|DCMM数据管理能力成熟度及各地政策汇总

信息技术与经济社会的交汇融合引发了数据爆发式增长。数据蕴含着重要的价值&#xff0c;已成为国家基础性战略资源&#xff0c;正日益对全球生产、流通、分配、消费活动以及经济运行机制、社会生活方式和国家治理能力产生重要影响。数据价值发挥的前提是管理好数据&#xff0c;…

断点重传、错误自动重传,优秀的文件传输工具应该具备这些特性

在当今的信息时代&#xff0c;文件传输是我们日常工作和生活中不可或缺的一项功能。无论是发送照片、视频、文档等个人文件&#xff0c;还是交换设计图、合同、报告等商业文件&#xff0c;我们都需要依靠各种文件传输工具来实现。但是&#xff0c;文件传输并不总是一帆风顺的&a…

innovus 报告多边形floorplan的boundary坐标

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f; 拾陆楼知识星球 历史文章: 常用dbGet命令 dbGet快速入门 使用"Cut Rectilinear"功能可以​调整floorplan形状&#xff0c;使其变成非矩形多边形&#xff08;polygon&#xff09;。​ …

干货文章|特殊区域在OSPF协议中的分析

我们都知道OSPF路由器协议是基于SPF算法计算最优路径&#xff0c;当用于SPF算法的LSDB的越大&#xff0c;那么路由器的计算压力就越大&#xff0c;对于一些性能不佳的OSPF路由器来说&#xff0c;如何尽可能地减小LSDB规模也就成了OSPF设计时要考虑到的问题&#xff0c;为了进一…

Vim学习(二)—— 编译C程序

打开终端&#xff0c;这里以MobaXterm为例&#xff0c; 邮件创建新的空文件并命名&#xff0c; 然后cd到对应路径下&#xff0c;用 vim hello.cvim打开创建的文件&#xff0c;进入编辑模式&#xff0c;编辑完程序后按Esc退出编辑模式&#xff0c;输入 :wq保存并退出&#xf…

C++——vector介绍及其简要模拟实现

vector的介绍 此主题介绍转载自(https://cplusplus.com/reference/vector/vector/) 1.vector是一个表示可变大小数组的序列容器 2.vector同数组一样&#xff0c;采用连续存储空间来存储元素&#xff0c;这样可以用下标来对vector中的元素进行访问&#xff0c;但是vector的大…

centos7 安装桌面

先装 xrdp $ sudo yum install -y epel-release $ sudo yum install -y xrdp $ sudo systemctl enable xrdp $ sudo systemctl start xrdp开防火墙端口 $ sudo firewall-cmd --add-port3389/tcp --permanent $ sudo firewall-cmd --reload比较喜欢 GNOME $ sudo yum groupin…

python自动化:系统凭据的获取与添加

在自动化流程开发中&#xff0c;我们经常会遇到输入帐号、密码的情况&#xff0c;帐号明文还可以&#xff0c;但是密码不想展示给他人&#xff0c;但是不想自己去手动输入怎么办&#xff1f; 基于以上情况我们可以使用windows自带的凭据管理器进行密码存储&#xff0c;其实我们…

数据结构与算法之时间空间复杂度

主要简介 1. 时间复杂度:运行一个程序所花费的时间。O() 2. 空间复杂度&#xff1a;运行程序所需要的内存 OOM&#xff0c;开了空间的地方&#xff0c; 比如 数组 链表&#xff0c;缓存对象&#xff0c;递归 时间复杂度表示方式 O(1),O(n),O(nlogn),O(n^2),O(n1),O(logn),O(n!…

造个轮子-任务调度执行小框架-任务清单执行器实现

文章目录 前言执行器流程提交流程线程池实现执行器实现接口状态标志执行周期实现清单代理创建清单项执行总结前言 okey,上一篇文章我们提到了,如何实现它的一个清单的一个代理。这里的话我们来捋一捋我们的这个执行流程是啥: 所以的话,我们的我们这里今天要做的是这个执行…

Jmeter - 函数助手

__StringFromFile StringFromFile函数用于获取文本文件的值&#xff0c;一次读取一行 1、输入文件的全路径&#xff1a;填入文件路径 2、存储结果的变量名&#xff08;可选&#xff09; 3、Start file sequence number (opt)&#xff1a;初始序列&#xff0c;例如从第3行开始读…

ATFX汇评:美7月通胀率数据基本符合预期,美指仍无法站稳103关口

ATFX汇评&#xff1a;据美劳工部&#xff0c;美国7月未季调CPI年率&#xff0c;最新值3.2&#xff0c;高于前值3%&#xff0c;低于预期值3.3%&#xff0c;这标志着连续12个月的下降已经停止&#xff1b;7月未季调核心CPI年率&#xff0c;最新值4.7%&#xff0c;低于前值4.8%&am…

Poco框架(跨平台自动化测试框架)

Poco基于UI控件搜索原理 &#xff0c;适用于Android、iOS原生和各种主流的游戏引擎应用。 中文官方文档&#xff1a;欢迎使用Poco (ポコ) UI自动化框架 — poco 1.0 文档 参考文档&#xff1a; Poco介绍 - Airtest Project Docs 环境准备 安装库&#xff1a;pip install po…

本地安装hadoop及其依赖组件

安装目录以及各个版本 大数据安装版本 软件版本备注hadoophadoop-3.3.4hdfs基础sparkspark-3.2.4-bin-hadoop3.2计算框架zookeeperapache-zookeeper-3.5.7-bin分布式服务器hbasehbase-2.4.11列式存储hiveapache-hive-3.1.3-bin数仓元数据 启动服务 su - hadoop -- 启动hadoo…

Vue3 nodejs 安装和配置---vue3教程一

文章目录 前言1、nodejs安装2、配置缓存路径&#xff1a;3、 阿里镜像cnpm使用4、 yarn安装5、配置nodejs国内镜像6、查看各个版本7、node npm yarn概念8、nodejs 和vue 关系外传 前言 本人主做后端Java&#xff0c;以前搞全栈开发是&#xff0c;还没有vue,rect等前端框架&…

如何解决物流投诉问题,拥有更多的回头客?

在电商物流中&#xff0c;客户投诉比较多的一块通常是配送延迟或派送问题。以下是一些可能导致此类问题的原因以及解决方法&#xff1a; 配送员数量不足或调度不合理&#xff1a;电商企业可能面临配送员不足的情况&#xff0c;导致派送时间延长或出现派送失败等问题。解决方法…

【写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串】

写一个函数&#xff0c;判断一个字符串是否为另外一个字符串旋转之后的字符串 1.题目 写一个函数&#xff0c;判断一个字符串是否为另外一个字符串旋转之后的字符串。 例如&#xff1a;给定s1 AABCD和s2 BCDAA&#xff0c;返回1 给定s1abcd和s2ACBD&#xff0c;返回0. AABCD左…

40 proc 文件系统

前言 在 linux 中常见的文件系统 有很多, 如下 基于磁盘的文件系统, ext2, ext3, ext4, xfs, btrfs, jfs, ntfs 内存文件系统, procfs, sysfs, tmpfs, squashfs, debugfs 闪存文件系统, ubifs, jffs2, yaffs 文件系统这一套体系在 linux 有一层 vfs 抽象, 用户程序不用…

为何不禁用危险的memcpy或更新memcpy源码,而使用更安全的memcpy_s

关于使用更安全的memcpy_s而不禁用危险的memcpy或更新memcpy源码的问题&#xff0c;以下是一些考虑因素&#xff1a;首先&#xff0c;memcpy_s并不是绝对安全的实现。尽管它要求您提供一个长度参数来确保不会发生溢出或越界问题&#xff0c;但这个长度仍然需要您自己提供&#…

液压系统比例阀放大器US-DAT2-F、US-DAPQ-N、US-DAS2

比例放大器US-DAS1、US-DAS2、US-DAPQ-N、US-DAPQ-H、US-P1、US-P2、US-DAT2-F、US-DAT2-A适配控制各种不带位置反馈比例阀&#xff1b; 控制如博世力士乐&#xff08;Bosch Rexroth&#xff09;、伊顿威格士&#xff08;EATON Vickers&#xff09;、油研&#xff08;YUKEN&am…