linux网络编程套接字编程基础,tcp和udp

预备知识

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

  我们已经知道了IP地址和Mac地址的区别,IP地址能标识唯一的一台主机。

  在我们发送消息的数据包头部中,有两个IP地址,分别叫做源IP地址和目的IP地址,因为这个很简单,我们只需要知道有这两个东西即可。

认识端口号

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

端口号是一个2字节16位的整数。

端口号是用来标识一个进程的,一个端口号能标识唯一的一个进程,由它来告诉OS当前数据要交给哪个进程处理。

IP地址 + 端口号能够标识网络上一台主机的一个进程(唯一的)。

   举一个抖音刷视频的例子

linux网络编程套接字编程基础,tcp和udp_第1张图片

  用户在客户端发送了一个获取视频的请求,假设这个客户端的端口号是4321,抖音服务器的端口号是8080,那么我们发送的请求经过层层封装,然后被服务器拿取后又经过层层解包和分用,但是服务器上那么多个进程,这怎么知道哪个进程才是来接收处理这个请求的呢?我们知道,几乎任何层协议都要在报头中提供,决定将自己的有效载荷交付给上层哪一种协议的能力,在传输层这里,就是通过端口号来实现的。

  不过说句题外话,我们客户端怎么知道服务端的端口号是多少呢?每一个服务的端口号必须是总所皆知的,精心设计的,被客户端所知晓的。其实有很多服务的端口号都是有规定的,有些服务的端口号就已经内置在客户端里面了,毕竟也是一个进程。

  还有就是关于在服务端,怎么通过端口号来找到进程,这其实简单来看就是用了一张类似哈希表的表来实现的。

  另外,一个端口号只能标识唯一的进程,而一个进程可以被多个端口号所标识

端口号vs进程pid

  进程pid可以标识唯一的一个进程,而端口号也是用来标识进程的。那么为什么不直接使用进程的pid作为网络通信里的端口号呢? 

  我们有没有想过,pid是OS里面的,如果真的使用pid作为端口号,那么一旦系统发生故障,或者是换系统了,那这些以原来的系统进程的pid作为端口号的进程怎么处理?

  所以明明pid已经能标识一个一台主机的进程的唯一性了,却还是要搞一个端口号的原因:

1.首先不是所有的进程都要进行网络通信,但是所有的进程都要pid。

2.就是刚刚所说的,系统和网络功能解耦。

认识TCP协议

TCP(Transmission Control Protocol 传输控制协议) 。

它的特点是:

传输层协议

有连接

可靠传输

面向字节流

认识UDP协议

UDP(User Datagram Protocol 用户数据报协议)

特点:

传输层协议

无连接

不可靠传输

面向数据报

简单总结TCP,UDP协议

这里对两种协议只是一个简单的直观的认识,后续还会有细节。

其中需要注意的是这个可靠传输,这其实是一个中性词,不分褒贬。就如同化学里的惰性气体一样,虽然叫做惰性,但是并不是贬义而是它的特性。

这里的可靠传输也是如此。

可靠传输就是对传入的数据不仅传过去了,还要根据接收方的反馈来检测这个数据是否在传输的过程中出现丢包,乱序等错误,并且在等待反馈的过程中,这个报头的信息就一直维护在传输层。

而不可靠传输就是直接把数据丢过去,是死是活都不管了,没有了检测,速度自然也就快很多。

因此二者并没有褒贬之分,只是应用场景不同,像银行支付宝这种,网络底层肯定得用TCP,而直播看视频这些就可以用UDP协议。 

网络字节序

  C语言阶段我们学过大端和小端,也就是内存中,多字节数据相对于内存地址有大端和小端之分。在磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分。那么在网络数据流中同样有大端和小端。

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。

因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节

不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

为使网络程序具有可移植性 , 使同样的 C 代码在大端和小端计算机上编译后都能正常运行 , 可以调用以下库函数做网络 字节序和主机字节序的转换。
linux网络编程套接字编程基础,tcp和udp_第2张图片

 这些函数名很好记,h表示host也就是主机,n表示network,也就是网络,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

端口号的其他补充

  端口号是一个无符号16位的整数,用于表示主机上的网络通信socket,因为是16位,因此最大端口号是65535(从0开始)。
  IP地址用于标识主机,端口号用于标识主机上的对应网络通信socket。
  

小端:低字节位数据存放在内存低地址处, 高字节位数据存放在内存高地址处。

大端:高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址。

网络字节序为大端字节序。

socket编程接口 

cocket常见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。
我们发现,上述接口,除了第一个接口,其他的接口的第二个参数都是一个统一的类型。
然而套接字其实不只有一种,它有三种:
1.域间套接字编程,是关于同一个主机内的多个进程通信的。
2.原始套接字编程,这个一般都是绕过了传输层,直接访问底层的接口,通常用来编写一些网络工具
3.网络套接字编程,一般就是用网络传输层,比如TCP这种来实现用户间
的网络数据通信
linux网络编程套接字编程基础,tcp和udp_第3张图片

但是网络设计者,想将网络接口统一抽象化,就是三个类型但是用一套接口。所以参数的类型必须是统一的,所有之前看到的接口第二个参数的类型都是一样的。

我们用的网络套接字编程用到的是图中第二个结构体,如果用域间套接字编程就是第三个结构体。

而我们看到上述接口中,第二个参数的类型是第一个结构体的类型,然后我们发现每一种结构体的前16位都是地址类型,因此其实这些接口内部都会根据这个结构体的前两个字节(16个bit位也就是两字节)来进行判断,判断我们是要网络通信还是本地通信。

所以今后我们在使用这个接口的时候,需要先对传入的参数进行强转。这个编程思想不是很像多态?

对于上述的总结:

1.

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

通常这个结构后面的14字节的数据是没什么用的。

linux网络编程套接字编程基础,tcp和udp_第5张图片 虽然socket api的接口是sockaddr,但是我们真正基于IPV4编程的时候,使用的数据结构是sockaddr_in,在这个结构体里主要有三部分信息,地址类型,端口号和IP地址。

如图中我们看到sockaddr_in里面还有一个结构体 in_addr,它是跟网络地址有关的

linux网络编程套接字编程基础,tcp和udp_第6张图片

但其实在这个结构体里面就只有一个32位的整数,来表示IPV4的IP地址。

补充知识:双#

linux网络编程套接字编程基础,tcp和udp_第7张图片


基于UDP协议的简单网络通讯(聊天室)

补充:在linux下快速用man查函数/接口 

  如果已经会用man指令了就没事了,可以跳过。但是学到这里还是不会用man查指令就应该开始习惯用man查指令了,而不是用csdn或者问gpt了。用man查函数可以查到该函数需要包含的头文件,参数类型,函数的作用等等。用自己用man查可以锻炼自己的独立解决问题能力。

  如果我们想查某个函数 比如  bezero(),那么我们就输入  man bezero,即可。

但是有些指令这样直接查可能查不到,比如bind(),那么我们就可以指定在2号手册查,比如:

man 2 bind  这样就可以查到了,2号手册一般就是要访问到内核层的接口,一些系统方面的函数。

对于几号手册可以简单看下

linux网络编程套接字编程基础,tcp和udp_第8张图片

另外,如果用man查不到函数,那么可能需要先更新一下man手册

可以执行 指令

sudo yum install -y man-pages

 如果不行,那么就是yum还没有配置。

 服务端的实现

UdpServer.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "log.hpp"

// using func_t = std::function;  //包装器
typedef std::function func_t;  //上面是C++11用法,二者等价

Log lg;

enum{
    SoCKET_ERR=1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer{
private:
    int sockfd_;    // 网络文件描述符
    std::string ip_;// bind不要固定绑定
    uint16_t port_; // 表明当前服务器的端口号
    bool isrunning_;
    std::unordered_map online_user_; // 哈希表存用户
public:
    UdpServer(const uint16_t &port = defaultport,const std::string &ip = defaultip)
    :sockfd_(0),port_(port),ip_(ip),isrunning_(false)
    {}
    void Init()
    {
        // 1. 创建udp socket
        sockfd_ = socket(AF_INET,SOCK_DGRAM,0);
        if(sockfd_ < 0)
        {
            lg(Fatal,"套接字创建失败,sockfd: %d",sockfd_);
            exit(SoCKET_ERR);
        }
        lg(Info,"套接字创建成功,sockfd: %d",sockfd_);
        //bind socket
        struct sockaddr_in local;
        bzero(&local,sizeof(local));  //初始化清零数据
        local.sin_family = AF_INET;
        local.sin_port = htons(port_); // 保证端口号是网络字节序
        local.sin_addr.s_addr = inet_addr(ip_.c_str());  //注意这里的IP
        //local.sin_addr.s_addr = htonl(INADDR_ANY);

        if(bind(sockfd_,(const struct sockaddr *)&local,sizeof(local)) < 0)
        {
            lg(Fatal,"绑定失败,errno: %d, errno string: %s",errno,strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info,"绑定成功,errno: %d,err string: %s",errno,strerror(errno));
    }
    void CheckUser(const struct sockaddr_in &client,const std::string clientip,uint16_t clientport)
    {
        auto iter = online_user_.find(clientip);
        if(iter == online_user_.end())
        {
            online_user_.insert({clientip,client}); //新用户就创建
            std::cout << "[" << clientip << ":" << "] 新用户加入." << std::endl;
        }
    }

    void Broadcast(const std::string &info,const std::string clientip,uint16_t clientport)
    {
        for(const auto &user : online_user_)
        {
            std::string message = "[";
            message += clientip;
            message += ":";
            message += std::to_string(clientport);
            message += "]#";
            message += info;
            socklen_t len = sizeof(user.second);
            sendto(sockfd_,message.c_str(),message.size(),0,(struct sockaddr*)(&user.second),len);
            //遍历用户表(哈希表),来向每个用户都发送信息。
        }
    }

    void Run()  //对代码进行分层
    {
        isrunning_ = true;
        char inbuffer[size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_,inbuffer,sizeof(inbuffer) - 1,0,(struct sockaddr*)&client,&len);
            if(n < 0)
            {
                lg(Warning,"recvform error,errno: %d,err string: %s",errno,strerror(errno));
                continue;
            }
            
            uint16_t clientport = ntohs(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);
            CheckUser(client,clientip,clientport);

            std::string info = inbuffer;
            Broadcast(info,clientip,clientport);
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0)
            close(sockfd_);
    }
};

Main.cc

#include "UdpServer.hpp"
#include 
#include 

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << "port[1024+]\n" << std::endl;
}

std::string Handler(const std::string &str)
{
    std::string res = "服务器拿到一则消息: ";
    res += str;
    std::cout << res << std::endl;
    return res;
}

std::string ExcuteCommand(const std::string &cmd)
{
    FILE *fp = popen(cmd.c_str(),"r");
    if(nullptr == fp)
    {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while(true)
    {
        char *ok = fgets(buffer,sizeof(buffer),fp);
        if(ok == nullptr) break;
        result += buffer;
    }
    std::cout << result << std::endl;
    pclose(fp);

    return result;
}

int main(int argc,char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t prot = std::stoi(argv[1]);

    std::unique_ptr svr(new UdpServer(prot));

    svr->Init();
    svr->Run();

    return 0;
}

另外为了避免用户发送消息和接收消息重叠,影响体验,我们在用两个用两个窗口,然后对标准错误输出进行重定向

Terminal.hpp

#include 
#include 
#include 
#include 
#include 
#include 

std::string terminal = "/*/";  // 这里写你系统本地的输入输出的文件路径

int OpenTerminal()
{
    int fd = open(terminal.c_str(),O_WRONLY);
    if(fd < 0)
    {
        std::cerr << "open terminal error" << std::endl;
        return 1;
    }
    dup2(fd,2);  //将打开的文件重定向到错入输出
    return 0;
}

makefile

udpserver:Main.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpserver

socket(): 

  首先在初始化时我们使用了这个函数来获取了一个网络文件描述符

linux网络编程套接字编程基础,tcp和udp_第9张图片 如图是参数和头文件包含,图中不难看出,第一个参数(领域)是用来选择我们的网络编程模式,如下 IPV4的选项是 AF_INET。所以我们选择了这个。

第二个参数是选择通信方式,我们发现,SOCK_DGRAM的描述是数据包,并且是不可靠传输,我们这次是基于UDP协议实现的,因此我们选择了SOCK_DGRAM。 

最后一个参数我们传0即可。

bezero()

linux网络编程套接字编程基础,tcp和udp_第10张图片 这个函数就是将我们传入s的前n个字节的内容设置为0。我们当时是这样用的。

 inet_addr()

linux网络编程套接字编程基础,tcp和udp_第11张图片 这是我们在设置IP的时候所用到的函数。

其中关于它的返回值:如果失败则返回零,成功则返回非零。

在这里我们通过传入字符串形式的IP地址,通过这个函数转换成二进制的IP地址

local.sin_addr.s_addr = inet_addr(ip_.c_str());  //注意这里的IP

bind()

linux网络编程套接字编程基础,tcp和udp_第12张图片   这个函数就是用来绑定端口号和IP的。我们再初始化的时候,用的是sockaddr_in这个结构体,并且将其内容进行了初始化,但是我们要清楚,再怎么初始化,我们依旧是在用户层进行的初始化,在内核中并没有进行更改,所以才需要用到能访问到内核的接口才能真正的设置成功。

  同样,这个函数成功返回非零,失败返回零。因此我们直接把它放在if判断中。

另外注意,在第二个参数中我们之前讲到要进行强转。

linux网络编程套接字编程基础,tcp和udp_第13张图片

关于绑定时的注意事项

  首先就是IP地址,如果我们用的是我们当前主机的公网IP,那么对于虚拟机可能会绑定成功,但是云服务器会绑定失败。

  这是因为,如果我们显式的绑定了一个IP,对于服务器来说,只有向这个IP发送的信息,我们的服务器才能接收到。而现在很多主机不一定只有一个IP地址,比如有些机器有多张网卡那么就会有多个IP地址。如果我们不显式的绑定某个特定的IP地址,那么只要是向这台主机发送的消息,我们的服务器都能收到(当然,别忘了端口号),所以在这里我们也没有绑定特定的IP地址,不然在云服务器默认下会绑定失败。

linux网络编程套接字编程基础,tcp和udp_第14张图片 所以在这里我们对于IP设置的是缺省参数,并且缺省值是零,在Main.cc中,我们也没有传入IP地址,所以默认就是零。

  还有就是关于端口号。在这里直接说结论,端口号是用户可以随意设置的,但是[0,1023]这个范围的端口号是系统端口号,如果我们是用户权限执行代码,也就是用用户去绑定系统端口号,会绑定失败,我们没有权限。但是我们可以用管理员身份来强行进行绑定。另外在这个范围之外,还有一些特殊的端口号也不能用,比如mysql的端口号就是3306。

  所以我们用户选端口号的时候,要注意范围。

htons()

这个函数太简单了,就简单说说。

在代码中,我们的端口号需要转换成网络字节序,因此需要用到这个函数,将端口号转换成16位的网络字节序。

recvfrom() 

linux网络编程套接字编程基础,tcp和udp_第15张图片 看参数就知道,这个是用来接收数据的,操作能类似文件那一套。对于flags我们传入0就好。

在这里,recvfrom是阻塞等待的接收信息。 

sendto()

 linux网络编程套接字编程基础,tcp和udp_第16张图片

  我们作为服务器,当然还可以发送信息,看参数跟recvfrom很相似,所以也不用多说。 

  不过有一点不同的就是最后这个参数,是关于长度的,sendto是直接传,而recvfrom则是传入指针。

popen() 

  popen函数是一个用于创建一个进程并和它建立管道连接的函数。它可以用来执行外部命令并获取命令的输出。popen函数通常用于创建一个子进程来执行一个外部命令,并且可以在父进程中读取或写入与子进程相关的标准输入、标准输出和标准错误。

linux网络编程套接字编程基础,tcp和udp_第17张图片

其中,command参数是要执行的外部命令的字符串,mode参数是指定popen的行为。mode可以是"r""w",表示使用popen函数来读取或写入子进程的标准输入和标准输出。

当调用popen函数时,它会创建一个用于执行command的子进程,并返回一个指向由子进程执行的命令的流指针。我们可以像使用普通文件流一样来读取或写入这个流指针。

当不再需要子进程时,可以使用pclose函数关闭由popen创建的流。pclose函数会等待子进程执行完毕,并返回子进程的退出状态。

需要注意的是,popen函数的使用是受到一定的系统限制的,如在Windows系统中的一些限制。同时,popen函数也可能存在一些安全风险,因为它可以执行用户输入的外部命令。

其他注意点 

 在析构函数这里,因为sockfd_本质还是文件描述符,所以用完要关闭。但其实这个不关闭也没事,毕竟文件的生命周期是随进程的,服务器关了,一般也是进程退的时候。

还有就是Main.cc这里

linux网络编程套接字编程基础,tcp和udp_第18张图片

我们的端口号是在启动服务器的时候传入的,因此注意启动这个进程的格式。如果格式不符合,那么进程会直接退出。 

这就是服务端。

关于 inet_ntoa

inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果 . 那么是否需要调用者手动释放呢?
linux网络编程套接字编程基础,tcp和udp_第19张图片

可以看下以下代码

linux网络编程套接字编程基础,tcp和udp_第20张图片

 

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

在APUE中, 明确提出inet_ntoa不是线程安全的函数;
但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。
在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

客户端

最难啃的骨头已经完了,在客户端就简单多了,没有什么新的函数了。

不过因为如果只有一个进程,那么输入方在getline那里会阻塞住,期间不能看到别的用户发消息,因此我们需要两个线程,一个线程发消息,一个线程接收消息,这样就可以解决这个问题。

 UdpClient.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Terminal.hpp"

using namespace std;

void Usage(string proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n" << endl;
}

struct ThreadData
{
    struct sockaddr_in server;
    int sockfd;
    std::string serverip;
};

void *recv_message(void *args)
{
    OpenTerminal();  // 先将标准错误输出进行重定向
    ThreadData *td = static_cast(args);
    char buffer[1024];
    while(true)
    {
        memset(buffer,0,sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        ssize_t s = recvfrom(td->sockfd,buffer,1023,0,(struct sockaddr*)(&temp),&len);
        if(s > 0)
        {
            buffer[s] = 0;
            cerr << buffer << endl;
        }
    }
}

void *send_message(void *args)
{
    ThreadData *td = static_cast(args);
    string messages;
    socklen_t len = sizeof(td->server);

    std::string welcome = td->serverip;
    welcome += " comming...";
    sendto(td->sockfd,messages.c_str(),messages.size(),0,(struct sockaddr*)&(td->server),len);

    while(true)
    {
        std:: cout << "请输入@ ";
        getline(cin,messages);
        sendto(td->sockfd,messages.c_str(),messages.size(),0,(struct sockaddr*)&(td->server),len);
    }
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    struct ThreadData td;
    bzero(&td.server,sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(td.server);

    td.sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(td.sockfd < 0)
    {
        cout << "创建套接字失败" << endl;
        return 2;
    }
    // 这里没有显式的bind,客户端的端口号有OS随机选择
    // 当我们首次发送数据的时候,系统再给我们bind。

    td.serverip = serverip;

    pthread_t recvr,sender;  //创建两个线程,一个负责发消息,一个负责处理消息
    pthread_create(&recvr,nullptr,recv_message,&td);
    pthread_create(&sender,nullptr,send_message,&td);

    pthread_join(recvr,nullptr);
    pthread_join(sender,nullptr);

    close(td.sockfd);
    return 0;
}

  这里需要注意的是,我们并没有显式的进行bind。其实,OS会帮我们进行bind。因为bind对于客户端来说无非就是设置端口号,而我们客户端作为发送方,自己的端口号是啥无关紧要,所以我们就不用bind了,交给系统随机生成端口号就行了。

当我们首次发送数据的时候,系统就会帮我们bind。

其他注意事项

  在linux中,我们可以执行以下命令来查我们的udp协议的服务端是否启动

netstat -nuap

 用以下可以查看tcp协议的

netstat -nltp

不加n也可以。

其中l就是listen,监听的意思,t就是tcp协议。

  另外对于云服务器的Linux,一般让客户端直接往云服务器主机的IP发信息可能发不了,我们可以用127.0.0.1这个地址,这个地址也叫做本地环回地址,本地环回地址是指主机上的网络接口设备用来发送数据到自身的虚拟地址,通常用来做像我们这样的客户端和服务端的测试。并且这个是IPV4下的本地环回地址,IPV6的本地环回地址是::1。

执行结果

这启动后就是两个进程,为了方便我们再修改一下makefile

.PHONY:all
all:udpserver udpclient

udpserver:Main.cc
	g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
	g++ -o $@ $^ -lpthread -std=c++11


.PHONY:clean
clean:
	rm -f udpserver udpclient

关于测试

  如果我们想让我们的客户端和服务端在不同的机器上进行测试,我们可以将客户端代码发给其他机器,其中我们可以用sz+(文件名)来进行发送。

  另外我们也可以尝试将客户端放在windows,服务端放在Linux下进行测试,不过在Windows下还需要对代码进行部分修改。

基于TCP协议的多线程网络单词翻译程序

  学过之前的UDP,那么学TCP就会简单很多,TCP相比UDP在代码上不过就是多了监听和链接,毕竟TCP是面向连接的。因为TCP是重头戏,所以我们不但使用了多线程,而且我们要学会如何将其进程守护化。并且服务端和客户端之间的通信用的还是文件的那一套,就是read和write。

关于大小端问题

  另外,我们有没有想过,ip地址和端口号,我们都有函数将其转成网络序列,或者从网络序列转主机系列,并解决大小端问题,但是我们发送的信息如果小端,对方接收的主机是大端怎么办呢?

但其实这个不用担心,我们使用的recvfrom和sendto这些函数已经帮我们处理了这些事情了,只是ip地址和端口号比较特殊,需要专门搞个这样的函数并且要用户手动转换。 

TcpServer.hpp 

  服务端代码

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;    // ?
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd,const std::string &ip, const uint16_t &p, TcpServer *t)
    : sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port, const std::string &ip = defaultip)
    : listensock_(defaultfd),port_(port),ip_(ip)
    {}

    void InitServer()
    {
        listensock_ = socket(AF_INET,SOCK_STREAM,0);  // 这里跟UDP一样,第一个参数依旧是选择IPV4,第二个参数不同,是选择可靠传输
        if(listensock_ < 0)
        {
            lg(Fatal,"创建套接字失败,errno: %d,errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info,"创建套接字成功,listensock_: %d",listensock_);

        int opt = 1;
        setsockopt(listensock_,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));  // 这里是为了防止偶发性的服务器挂掉后,无法立即重启

        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);  //主机序列转网络序列 s代表短,16位
        inet_aton(ip_.c_str(),&(local.sin_addr));  // 在UDP那里,我们用的是inet_ntoa()

        if(bind(listensock_,(struct sockaddr *)(&local),sizeof(local)) < 0)
        {
            lg(Fatal,"绑定失败,errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg(Info,"绑定成功,listensock_: %d", listensock_);
        // 到这里跟UDP几乎差不多
        // 因为Tcp是面向连接的,服务器一般是比较“被动的”,所以服务器一直处于一种等待连接到来的状态

        if(listen(listensock_,backlog) < 0)
        {
            lg(Fatal,"监听失败,errno: %d,errstring: %s",errno,strerror(errno));
            exit(ListenError);
        }
    }

    void Start()
    {
        //Daemon();
        ThreadPool::GetInstance()->Start();
        lg(Info,"tcpServer 正在运行...");
        while(true)
        {
            // 1.获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr *)&client,&len);
            if(sockfd < 0)
            {
                lg(Warning,"连接失败,errno: %D, errstring: %s", errno,strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));
            
            // 根据新连接来通信
            lg(Info,"get a new link...,sockfd: %d, client ip: %s, clint port: %d",sockfd, clientip, clientport);

            // 然后交给线程们去做
            Task t(sockfd, clientip, clientport);
            ThreadPool::GetInstance()->Push(t);
        }
    }

    ~TcpServer() {}
private:
    int listensock_;  // 这是监听用的文件描述符
    uint16_t port_;
    std::string ip_;
};

  在代码中可以看出,我们的TCP服务端它是单次服务的,也就是当有一个请求出现时,先链接,然后处理,完事后会断开链接,再去等待其他链接。

listen()

  将我们的套接字设置为listen状态,设置好后,那么就可以通过这个套接字等待新连接的到来。

linux网络编程套接字编程基础,tcp和udp_第21张图片 linux网络编程套接字编程基础,tcp和udp_第22张图片

首先是返回值,成功返回0,失败返回-1。所以我们直接把它放在了if里。 

第一次参数也就是我们之前的fd,第二个参数先作为伏笔,以后再说,只是一般backlog的值不宜设置过大。

accept()

linux网络编程套接字编程基础,tcp和udp_第23张图片

 看参数就很好懂,我们是通过这个函数,来接收来自客户端的连接的。

 关于返回值

成功会返回一个文件描述符,失败则返回-1,其中错误码已被设置。

  但是不对啊,为什么还会返回一个文件描述符呢?之前不是已经有了一个文件描述符吗?就是listensockfd_。 

  这就好比一个餐馆,餐馆里面有厨师,负责做菜;餐馆外面有一个拉客的,负责在街上吸引客人;二者是有分工的。也就是说,这两个文件描述符各司其职,大致关系是这样

linux网络编程套接字编程基础,tcp和udp_第24张图片

Task.hpp

  因为要用到线程池,所以我们对任务也进行了封装

#pragma once
#include 
#include 
#include "Log.hpp"
#include "Init.hpp"

extern Log lg;
Init init;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
    : sockfd_(sockfd), clientip_(clientip),clientport_(clientport)
    {}

    Task() {}

    void run()
    {
        char buffer[4096];
        ssize_t n = read(sockfd_, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "client key# " << buffer << std::endl;
            std::string echo_string = init.translation(buffer);
            n = write(sockfd_, echo_string.c_str(), echo_string.size()); // 这里的sockfd_就是之前accept()返回的文件描述符
            if(n < 0)
            {
                lg(Warning,"write error,errno: %d,errstring: %s", errno, strerror(errno));
            }
        }
        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, client ip: %s, client port: %d", sockfd_, clientip_.c_str(),clientport_);
        }
        close(sockfd_);  // 在这里,我们用完了就关闭了
    }

    void operator()()
    {
        run();
    }

    ~Task() {}
Private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};

ThreadPool.hpp

  线程池虽然以前实现过了,是基于懒汉方式的生产者消费者模型实现的,这里还是贴出来作为复习好了。

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defaultnum = 5;

template 
class ThreadPool
{
private:
    std::vector threads_; // 使用数组管理线程
    std::queue tasks_;             // 队列里面放任务

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool *tp_;  // 新增的静态的该对象的指针变量。
    static pthread_mutex_t lock_;  // 需要锁来保证在懒汉方式下实现的单例模式的线程安全
private:
    ThreadPool(int num = defaultnum) : threads_(num)  //构造和析构私有,就不能直接在栈或者通过new在堆上创建对象
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool &) = delete;
    const ThreadPool &operator=(const ThreadPool &) = delete;
public:
    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 &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "没有这个线程";
    }

public:
    
    static void *HandlerTask(void *args) // 特别注意,这个方法是静态的!
    {
        ThreadPool *tp = static_cast *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty()) // 同样使用循环来防止伪唤醒的情况
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t(); // 处理任务放在加锁之后,使处理操作和临界区的代码可以并行进行
            std::cout << name << " 运行, "
                      << "结果: " << t.GetResult() << std::endl;
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "线程-" + std::to_string(i);
            pthread_create(&threads_[i].tid, nullptr, HandlerTask, this); // 注意这里参数传的使this指针!
        }
    }
    T Pop()
    {
        T t = tasks_.front(); // 这里不用加锁,因为调用它的地方已经在锁里面了
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    
};

template                                        //在类外定义
ThreadPool *ThreadPool::tp_ = nullptr;

template
pthread_mutex_t ThreadPool::lock_ = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER;

 Main.cc

#include 
#include 
#include "TcpServer.hpp"

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }
    uint16_t port = std::stoi(argv[1]);
    lg.Enable(Classfile);  // 一般服务器的打印信息都是放在文件里面的
    std::unique_ptr tcp_svr(new TcpServer(port));
    tcp_svr->InitServer();
    tcp_svr->Start();

    return 0;
}

前台进程和后台进程以及会话

  这些是守护进程的前提知识。我们之前学过,我们在使用linux的时候,有分前台进程和后台进程。今天我们还需要知道的就是,当我们登录Linux系统的时候,系统会给我们起一个会话(session),然后在这个会话里面,分为一个前台进程和一个后台进程,这个系统默认分配给我们的前台进程就是bash进程,所以我们通过键盘输入ls等这样的命令,bash可以接收到ls,并创建子进程来执行命令。 

linux网络编程套接字编程基础,tcp和udp_第25张图片

  所以我们把判断一个进程是前台进程还是后台进程的依据就是,这个进程是否能从键盘文件中读取数据。

  一般来说,我们执行一个进程,它默认是前台进程,因为我们发现这个进程可以从键盘中读取数据,但此时ls等命令就不管用了,这是因为bash暂时被放在了后台,暂时变成了后台进程。

  如果我们想让一个进程作为后台程序启动,我们可以在启动命令的末尾加上 &。 

  另外我们可以思考一下,我们让一个进程变成了前台进程,让bash先到后台,然后我们使用信号暂停了这个前台进程,那么此时这个进程依旧会是以前台进程的身份暂停吗?答案是不会的,因为如果暂停了没有解除暂停的手段,那么我们按什么键没有反应,因为bash此时在后台,接收不到键盘上数据,如果真是这样,那么此时操作系统大概率就会挂掉了。所以如果我们把前台进程暂停了,那么这个进程就会被放到后台,bash重新成为前台进程。

  另外挂在后台的进程是有任务号的

linux网络编程套接字编程基础,tcp和udp_第26张图片

任务和进程组

  我们之前从没有谈过进程组,对任务的理解也比较狭隘。其实任务是可以由多个进程来完成的,只是我们一般都只看到一个进程完成一个任务,而多个进程去做同一个任务,那么就可以把这些进程划分成一个组。 

linux网络编程套接字编程基础,tcp和udp_第27张图片

所以之前我们看到的 PGID就是进程组ID, SID就是sessionID。

说到会话,再说说注销,所谓注销就是将用户退出,注销不是关机,它是把新起的会话删掉,所以我们起的任务也会被删掉。

为什么要守护进程化? 

  因为真正的服务器都是长期运行的,不能说有一个用户登录或者注销一下就会影响服务器,守护进程化就是让这个进程不受到用户登录或注销的影响。

  所谓守护进程化就是单独给这个进程开一个会话,这样就不会受到其他用户登录和注销的影响了。原理就是,让这个进程成为孤儿进程,那么它就会被操作系统接管,跟用户就没关系了

linux网络编程套接字编程基础,tcp和udp_第28张图片

setsid() 

  这个函数非常简单,没有参数。作用就是将当前的进程另起一个会话,但是注意,组长进程是不可以另起会话的,所以我们才需要将这个进程变成孤儿进程后再调用。 

Daemon.hpp 

  虽然在库中有将进程守护化的函数,但是其实大多数情况下还是程序员自己实现一个,在这里也可以方便我们更好理解其原理。

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = "")
{
    //1.忽略其他异常信号
    signal(SIGGLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTPO, SIG_IGN);

    //2.将自己变成独立的会话
    if(fork() > 0)
        exit(0);  // 如果是父进程,那么就会直接退出,子进程继续向后执行,并且已经成为孤儿进程了
    setsid();     // 这个函数就是让这个进程成为一个新的会话

    // 3.更改当前调用进程的工作目录
    if(!cwd.empty())
        chdir(cwd.c_str());  // 因为大多数下,服务器是安装在系统上的,所以工作目录也就是在根目录下
    
    // 4.将标准输入,标准输出,标准错误重定向至/dev/null,这个文件也就是垃圾处理文件
    int fd = open(nullfile.c_str(),O_RDWR);
    if(fd > 0)
    {
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
        close(fd);   // 最后关掉这个文件描述符
    }
}

 在服务器中,如果有不小心用到cout这些函数,那么就会将信息打印到显示器。为了避免这样的情况,我们可以将0,1,2这三个文件描述符重定向至linux中一个专门处理垃圾信息的文件,我们将其重定向到这个文件中即可解决问题。

Init.hpp

  我们用哈希表来存单词

#pragma once

#include 
#include 
#include 
#include 
#include "Log.hpp"

extern Log lg;

const std::string dictname = "./dict.txt";
const std::string sep = ":";

static bool Split(std::string &s, std::string *part1,std::string *part2)
{
    auto pos = s.find(sep);
    if(pos == std::string::npos) return false;
    *part1 = s.substr(0,pos);
    *part2 = s.substr(pos + 1);
    return true;
}

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if(!in.is_open())
        {
            lg(Fatal,"ifstream open %s error",dictname.c_str());
            exit(1);
        }
        std::string line;
        while(std::getline(in,line))
        {
            std::string part1,part2;
            Split(line,&part1,&part2);
            dict.insert({part1,part2});
        }
        in.close();
    }

    std::string translation(const std::string &key)
    {
        auto iter = dict.find(key);
        if(iter == dict.end())
            return "Unknow";
        else
            return iter->second;
    }
private:
    std::unordered_map dict;
};

 其中,npos是一个static成员常量,它在C++标准库的string类中定义。它的值通常是一个特定的标记,表示无效或无法找到的位置。在std::string类中,npos被定义为-1,表示未找到指定的子字符串或子字符串的位置。在Split函数中,当查找分隔符的位置失败时,用npos来表示未能成功分割字符串。

  还有std::ifstream,是C++标准库中用于从文件中读取数据的类。通过使用std::ifstream类,可以打开文件并从中读取数据,例如文本文件或二进制文件。

TcpClient.hpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 

void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server,0,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;  //重连次数
        int isreconnect = false;  // 是否要重连
        int sockfd = 0;
        sockfd = socket(AF_INET,SOCK_STREAM,0);
        if(sockfd < 0)
        {
            std::cerr << "socker error" << std::endl;
            return 1;
        }
        do
        {
            //tcp客户端同样不需要显式的bind,是系统进行bind,随机端口号
            //在客户端发起connect的时候,进行自动随机bind
            int n = connect(sockfd,(struct sockaddr *)&server,sizeof(server));
            if(n < 0)
            {
                isreconnect = true;  // 说明此时需要重连
                cnt --;
                std::cerr << "connect error...,reconnect: " << cnt << std::endl;
                sleep(2);  //模拟重连时间
            }
            else
            {
                break;
            }
        }while(cnt && isreconnect);

        if(cnt == 0)  //重连次数耗尽
        {
            std::cerr << "user offline..." << std::endl;
            break;
        }

        std::string message;
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = write(sockfd,message.c_str(),message.size());
        if(n < 0)
        {
            std::cerr << "write error..." << std::endl;
        }

        char inbuffer[4096];
        n = read(sockfd,inbuffer,sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        close(sockfd);
    }

    return 0;
}

客户端这里我们加入了重连机制,新函数就是那个connect,但是这个很简单,跟服务端的accept一个样。

一个小技巧

  有时候我们做完一个服务端,我们想对这个服务端进行测试,但是我们又没有写好客户端,此时我们可以用

telnet +ip +端口号

 来访问我们的服务端,做简单的输入测试。

查看当前网络进程的ip和端口号

  方法有很多,我们这里以每秒打印一次的频率查看可以以下命令

watch -n 1 'netstat -tulnp | grep tcpserverd'

 如果要查看pid等信息可以用以下命令

watch -n 1 'ps aux | grep tcpserverd'

测试结果

linux网络编程套接字编程基础,tcp和udp_第29张图片 首先用每秒监视我们启动的这个服务端,然后先用telnet测试了一下,不是很准确。我们再用客户端测试一下

linux网络编程套接字编程基础,tcp和udp_第30张图片

linux网络编程套接字编程基础,tcp和udp_第31张图片 信息也是打印在文件里,并没有在显示器上。

此时,就算我们退出我们的Xshell,这个服务端还是存在,除非我们手动杀掉这个进程。

在Windows下的客户端

  很多时候,用户用的系统并不是LInux,比如很多用户用的是Windows系统,那么我们的一般将服务器放在Linux,客户端放在Windows,所以,我们可以对代码进行修改,使其在Windows下运行,这项能力是必须要锻炼的。

#include 
#include 
#include 
#include 
#include    //注意,WinSock2最好在Windows前面,不然可能会出幺蛾子,编译都不通过
#include 

#pragma warning(disable:4996) // 禁掉一些警告

#pragma comment(lib,"ws2_32.lib")

uint16_t serverport = 8888;  //端口号
std::string serverip = "110.41.35.225";  // 服务端ip

int main()
{
	WSADATA wsd;
	WSAStartup(MAKEWORD(2, 2), &wsd);

	struct sockaddr_in server;
	memset(&server, 0, sizeof(server));
	server.sin_family = AF_INET;
	server.sin_port = htons(serverport);
	server.sin_addr.s_addr = inet_addr(serverip.c_str());  //这里跟linux的处理几乎一样

	SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 注意是tcp
	if (sockfd == SOCKET_ERROR)
	{
		std::cerr << "创建套接字失败" << std::endl;
		exit(1);
	}

	std::string message;
	char buffer[1024];
	while (true)
	{
        int cnt = 5;  //重连次数
        int isreconnect = false;  // 是否要重连
        int sockfd = 0;
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socker error" << std::endl;
            return 1;
        }
        do
        {
            //tcp客户端同样不需要显式的bind,是系统进行bind,随机端口号
            //在客户端发起connect的时候,进行自动随机bind
            int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));  //连接操作也是一样
            if (n < 0)
            {
                isreconnect = true;  // 说明此时需要重连
                cnt--;
                std::cerr << "connect error...,reconnect: " << cnt << std::endl;
                //sleep(2);  //模拟重连时间
            }
            else
            {
                break;
            }
        } while (cnt && isreconnect);

        if (cnt == 0)  //重连次数耗尽
        {
            std::cerr << "user offline..." << std::endl;
            break;
        }

        std::string message;
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = send(sockfd, message.c_str(), message.size(), 0);  // 这里不再是用write了,而是send,下面同理
        if (n == SOCKET_ERROR) 
        {
            int err = WSAGetLastError();
            // 处理错误...   注意这里Windows的处理方式
        }

        char inbuffer[4096];
        int m = recv(sockfd, inbuffer, sizeof(inbuffer), 0);
        if (m > 0)
        {
            inbuffer[n] = 0;  
            std::cout << inbuffer << std::endl;
        }
        else if (m == 0)
        {
            std::cout << "Connection closed by peer." << std::endl;
        }
        else
        {
            int err = WSAGetLastError();
            std::cerr << "recv failed with error: " << err << std::endl;
        }
        closesocket(sockfd);
    }

	return 0;
}

 执行结果:

在Linux的服务器上确实收到了连接,

linux网络编程套接字编程基础,tcp和udp_第32张图片   但是因为编码原因,客户端这边的返回值出现了很多乱码,但是不影响,客户端和服务端是可以正常连接的。 

  而且要注意的是,在Windows下,无论是UDP还是TCP都使用recv函数来接收数据,只是在使用时需要注意协议的特点,send同理。

TCP协议通信流程

  TCP的三次握手和四次挥手

  这里主要是简单说下,TCP协议的三次握手,和四次挥手。

linux网络编程套接字编程基础,tcp和udp_第33张图片   三次握手是在客户端向服务端发起连接的时候,四次挥手是在双方断开连接的时候。

TCP的全双工 

TCP通信是全双工的,也就是可以同时进行读写。

只要这个主机支持TCP协议,那么在系统级别上一定会有发送缓冲区和接收缓冲区,所以为什么是全双工就是因为它的发送和接收是分开的。 

linux网络编程套接字编程基础,tcp和udp_第34张图片

  我们在创建好套接字后,OS就会在系统层面给我们创建对应的文件描述符的缓冲区,在用户层,我们就通过这个文件描述符来使用这个缓冲区。 

我们通过套接字,将内容写入到TCP协议的缓冲区中,再发送给对方。读取也是从接受缓冲区中,读取,然后再拷贝到用户层的缓冲区中。

资源在底层是分离的,互不影响,这就是全双工。UDP是没有发送缓冲区的。所以在这里read和write看起来就像是拷贝函数,把内核数据拷贝到用户缓冲区,或者将用户缓冲区的内容拷贝到内核缓冲区。

而发送/接受缓冲区它们打算怎么发,发多少,完全由TCP决定,所以TCP也叫做传输控制协议。

另外我们说TCP是面向连接的,在服务端启动时,时时刻刻会有很多连接连上来,那么操作系统肯定也要管理这些连接,怎么管理,还是先描述再组织。

补充:

IP协议是网络层协议,TCP和UDP协议在网络层都是基于IP协议的。(实现数据报传输)。

你可能感兴趣的:(网络,服务器,linux)