【网络编程】网络编程套接字(三)TCP网络程序

这篇具有很好参考价值的文章主要介绍了【网络编程】网络编程套接字(三)TCP网络程序。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

简单的TCP网络程序

一、服务器创建套接字

与前边的UDP网络程序相同,创建套接字的接口都是socket,下边对socket接口进行介绍:
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
协议家族选择AF_INET,因为我们要进行网络通信。
而第二个参数,为服务类型,传入SOCK_STREAM,我们编写TCP程序,所以要选择流式的服务。
第三个参数默认传入0,由前两个参数就可以推出这是基于TCP的网络程序。

// 创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            logMessage(FATAL, "create socket error %d-%s", errno, strerror(errno));
            exit(2);
        }

socket接口如果创建成功返回0,失败返回-1,并且错误码被设置,所以当返回值小于0时退出程序。

二、服务器绑定套接字

还是与UDP相同,绑定套接字需要bind接口,我们再次对bind接口进行学习:
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议

第一个参数传入前边创建的套接字,也就是一个文件描述符。
第二个参数是一个sockaddr类型结构体的地址,内边存储着要绑定的IP和端口号的相关信息。
第三个参数为结构体的大小。

       // bind绑定套接字
        struct sockaddr_in local;
        // bzero((void*)&local,sizeof(local));
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
       // local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_sock, (struct sockaddr *)&local, (socklen_t)sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind socket error %d-%s", errno, strerror(errno));
            exit(3);
        }

但是同时要注意网络序列和主机序列的转换,并且在处理IP地址时,也要注意到点分十进制与二进制的转换。

三、服务器监听

由于TCP协议是需要连接的,而UDP是不需要连接的,所以在对TCP的服务器进行创建,绑定套接字之后,必须进行监听操作,使服务器处于监听状态。这就例如:
一个商店老板,即使这会没有人来买东西,也必须坐在店里边,处于监听状态,一旦有人来买东西,就可以立马为客户服务。

一旦listen调用成功,服务器就会处于监听状态。

【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
sockfd:需要设置为监听状态的套接字对应的文件描述符。
backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。
返回值:如果监听失败返回-1,并且错误码被设置,成功返回0.

// 监听
        if (listen(_sock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen socket error %d-%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init success,sockfd: %d", _sock);

当监听完成之后,服务器的初始化才算完成。

四、服务器获取连接

当服务器初始化完成之后,此时就要让客户端来连接,必须通过accept来获取连接,当客户端发送连接请求之后,服务器和客户端的连接才正式完成。
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
参数:
sockfd:监听套接字的文件描述符
addr:对端网络的相关信息结构体,例如IP,端口号,协议家族等
addrlen:addr结构体的大小
返回值:
accept的返回值有一些不同,如果返回成功,这些系统调用返回一个非负整数,它是一个描述符对于接受的套接字。如果出现错误,则返回-1,并适当地设置errno。

那么这个返回值是什么意思呢?为什么会有两个文件描述符,他们之间有什么关系?

当我们使用accpet进行连接时,是通过监听套接字进行连接的,但是当连接上对端网络之后,不是监听套接字来提供服务的,而是返回成功之后,会返回一个套接字的文件描述符,是由该服务套接字提供服务的。

  • 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  • accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。
    下边通过一个例子来解释他们之间的关系:

当我们前往西安旅游时,一定想尝一尝正宗的羊肉泡馍,有一家店,服务员张三非常热情,一定在门口招呼路上的游客进去,当有一个游客准备进入餐馆吃饭时,张三就会喊一声,李四来人了,快出来招呼,但是张三又回到门口,继续让来往的游客进入餐馆,当下一个游客进入餐馆时,张三就说,王五来人了快来招呼人,此时服务顾客的人就是王五,而张三继续去外边找客人。

此处的张三我们就可以认为是监听套接字,主要功能就是不断的在外边找顾客,让顾客进入店内,也就是不断的获取新连接,而后边的李四王五赵六等等就相当于accept返回的服务套接字,他们才是为对端提供服务的。

// 建立连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int fd = accept(_sock, (struct sockaddr *)&src, &len);
if (fd < 0)
{
 	 logMessage(FATAL, "accept error %d-%s", errno, strerror(errno));
     continue;
}

五、服务器处理请求

通过以上的步骤,创建套接字,绑定套接字,监听,获取连接之后,当客户端对服务器进行连接之后,服务器就可以处理客户端发来的请求。

void service(int fd, const std::string &client_ip, const uint16_t &client_port)
{
    char buffer[1024];
    while (1)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
            break;
        }
        else
        {
            logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
            break;
        }
        write(fd, buffer, strlen(buffer));
    }
    close(fd);
}

由于套接字在系统层面来看,就是打开的文件,所以对文件进行读写就可以使用我们之前学习过的 read和write接口。
read
读取数据时,使用read接口。
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议

参数:
fd:文件描述符,表示从哪一个套接字中读取
buf:数据的存储位置,把数据读取到哪一个数组中
count:读取数据的大小
返回值:
当读取成功时,返回读取到的字节数,当写端关闭时,返回0,当读取错误时,返回小于0.

当返回值为0时,表示读取对端关闭了?

网络通信与进程间通信类似,和之前对文件读取写出相同:

  • 写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。
  • 读端进程不读,写端进程一直写,此时当缓冲区被写满后写端进程就会被挂起,因为此时空间没有就绪。
  • 写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。
  • 读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。

此处的情况就是写端也就是客户端将数据写完后将写端关闭,此时读端也就是服务器将数据读完之后就会读到0,所以返回值为0。


write
写入数据到网络时,需要使用write接口。
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
参数:
fd:写端套接字的文件描述符
buf:需要写入的数据
count:需要写入数据的字节数
返回值:
写入成功返回写入的字节数,写入失败返回-1,同时错误码被设置。


六、对服务器进行简单测试

当服务器初始化已经处理请求都完成之后,虽然还没有实现客户端,但是也可以telnet指令远程连接该服务器,实现请求处理服务:

第一步:
运行服务器,必须加上端口号,此时处于监听状态,等待客户端连接。
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
此时可以使用netstat指令观察该套接字的状态:
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
可以发现此时的服务器处于listen状态。

第二步:
使用telnet指令对服务器进行连接。

【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议

第三步:
对服务器进行请求。
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议

七、客户端创建套接字

与前边创建套接字没有什么区别,注意的就是使用流式传输。
客户端是不需要绑定IP和端口号的,在客户端在连接时,系统会自动给客户端分配。
客户端也不需要监听,因为客户端不会被主动连接。

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
}

八、客户端连接服务器

客户端要发送请求时,必须要知道服务器的IP地址和端口号,所以我们使用命令行参数的方式,将服务器的Ip地址和端口号传给客户端,客户端接收之后之后,将IP地址和端口号传入addr结构体中,然后使用connect接口进行连接。
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议

	struct sockaddr_in peer;
    memset(&peer,'\0', sizeof(peer));
    peer.sin_family = AF_INET;
    peer.sin_port = htons(server_port);
    peer.sin_addr.s_addr = inet_addr(server_ip.c_str());
    // 连接套接字
    if (connect(sock, (struct sockaddr *)&peer, (socklen_t)sizeof(peer)) < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }
    std::cout << "connect success" << std::endl;

connect接口如果调用成功,客户端会被随机分配一下端口号,只要可以唯一标识客户端即可。

九、客户端发起请求

客户端与服务器连接成功之后,使用send接口发送数据,如果发送成功,返回值大于0。当发送成功之后,使用recv接口接收数据,最后在收到的数据后加上’\0’,将字符串回显。

    while (true)
    {
        std::string line;
        std::cout << "请输入# " << std::endl;
        getline(std::cin, line);
        if (line == "quit")
            break;
        ssize_t s = send(sock,line.c_str(),line.size(),0);
        if(s>0)
        {
            char buffer[1024];
            ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
            if(s > 0)
            {
                buffer[s]=0;
                std::cout<<"回显#"<<buffer<<std::endl;
            }
            else if(s==0)
            {
                break;
            }
            else
            {
                break;
            }
        }
    }

十、服务器客户端测试

服务端
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议

客户端
【网络编程】网络编程套接字(三)TCP网络程序,网络编程,网络,tcp/ip,网络协议
当在客户端发起请求之后,客户端会与服务器建立连接,此时再次输入数据,服务器会对收到的数据进行回显。

多进程的TCP服务器

如果是单进程的服务器,当多个客户端同时启动,服务器只能处理一个客户端的请求,只有当第一个客户端退出之后,才会收到第二个客户端的请求。


为什么可以使用多进程
由于创建子进程后,子进程会继承父进程的文件描述符等信息,所以父进程创建的套接字也会被子进程继承下来,当我们使用多进程时,子进程就可以看到建立链接的文件描述符,并且当某一个进程处理完毕之后关闭文件描述符,也不会影响到其他的进程,因为父子进程具有独立性,在修改时会进行写时拷贝。


等待子进程问题
在子进程处理请求完毕之后,父进程必须等待子进程,要不然就会造成僵尸问题,会造成内存泄露,等待子进程有两种方式:

  1. 阻塞等待
  2. 非阻塞等待
    如果使用阻塞等待,那么说明父进程必须在等待第一个子进程服务完毕之后才可以处理下一个请求,本质上还是进行串行操作,并没有真正实现多进程。
    而如果使用非阻塞等待,虽然可以再进行其他的连接,但是必须不断的检测子进程是否退出。

为了解决以上的问题,我们可以采取两种方法:

  1. 对SIGCHLD进行自定义捕捉,主动忽略SIGCHLD信号,当子进程退出时,就会主动释放僵尸进程,父进程不会进行等待。
  2. 创建子进程,再让子进程创建子进程,让孙子进程进行服务,将子进程直接退出,当孙子进程处理完毕之后,成为孤儿进程被操作系统回收,所以父进程不需要进行等待。

一、忽略SIGCHLD信号

signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态.

//version1.0多进程版,对信号进行忽略
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                close(_sock);
                service(fd, client_ip, client_port);
                exit(0);
            }
            close(fd);

二、孙子进程提供服务

先创建子进程,再让子进程创建子进程,让孙子进程提供服务,但是将子进程退出,当孙子进程提供完服务之后,被操作系统回收。

void service(int fd, const std::string &client_ip,
                    const uint16_t &client_port)
{
    char buffer[1024];
    while (1)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
            break;
        }
        else
        {
            logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
            break;
        }
        write(fd, buffer, strlen(buffer));
    }
    close(fd);
}
//version1 .1多进程版,使用孙子进程进行服务 
pid_t id = fork();
if (id == 0)
{
    close(_sock);
    if (fork() > 0)
        exit(0);
    else
    {
        service(fd, client_ip, client_port);
        exit(0);
    }
}
waitpid(id, nullptr, 0);
close(fd);

多线程TCP服务器

服务器为了同时给多个客户端提供服务,不仅可以使用多进程来进行服务,也可以使用多线程来提供服务。
由于线程的回调函数中需要多个变量,所以我们将需要的IP,端口号,文件描述符写入一个类中,将实例化的对象指针传入回调函数,在回调函数中使用pthread_detach接口实现线程分离,主线程这边就不需要进行join回收线程了。文章来源地址https://www.toymoban.com/news/detail-578394.html

//服务函数
void service(int fd, const std::string &client_ip,
             const uint16_t &client_port)
{
    char buffer[1024];
    while (1)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
            break;
        }
        else
        {
            logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
            break;
        }
        write(fd, buffer, strlen(buffer));
    }
    close(fd);
}

//线程数据
class pthreadData
{
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
};

// version2多线程版
pthreadData *pd = new pthreadData();
pd->_port = client_port;
pd->_sock = fd;
pthread_t tid;
pthread_create(&tid, nullptr, Routine, pd);

//线程回调函数
static void *Routine(void *args)
{
    pthread_detach(pthread_self());
    pthreadData *pd = (pthreadData *)args;
    std::string client_ip = pd->_ip;
    uint16_t client_port = pd->_port;
    int sock = pd->_sock;
    service(sock, client_ip, client_port);
    return nullptr;
}

到了这里,关于【网络编程】网络编程套接字(三)TCP网络程序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 网络编程套接字( TCP )

    目录 1、实现一个TCP网络程序(单进程版)         1.1、服务端serverTcp.cc文件                  服务端创建套接字                  服务端绑定                  服务端监听                  服务端获取连接                  服务

    2024年01月17日
    浏览(40)
  • 【JavaEE】网络编程之TCP套接字、UDP套接字

    目录 1.网络编程的基本概念 1.1为什么需要网络编程  1.2服务端与用户端 1.3网络编程五元组  1.4套接字的概念 2.UDP套接字编程 2.1UDP套接字的特点  2.2UDP套接字API 2.2.1DatagramSocket类 2.2.2DatagramPacket类  2.2.3基于UDP的回显程序 2.2.4基于UDP的单词查询  3.TCP套接字编程 3.1TCP套接字的特

    2023年04月20日
    浏览(46)
  • 【JaveEE】网络编程之TCP套接字、UDP套接字

    目录 1.网络编程的基本概念 1.1为什么需要网络编程  1.2服务端与用户端 1.3网络编程五元组  1.4套接字的概念 2.UDP套接字编程 2.1UDP套接字的特点  2.2UDP套接字API 2.2.1DatagramSocket类 2.2.2DatagramPacket类  2.2.3基于UDP的回显程序 2.2.4基于UDP的单词查询  3.TCP套接字编程 3.1TCP套接字的特

    2023年04月13日
    浏览(44)
  • 【Linux网络】网络编程套接字(TCP)

    目录 地址转换函数 字符串IP转整数IP 整数IP转字符串IP 关于inet_ntoa 简单的单执行流TCP网络程序 TCP socket API 详解及封装TCP socket  服务端创建套接字  服务端绑定  服务端监听  服务端获取连接  服务端处理请求 客户端创建套接字 客户端连接服务器 客户端发起请求 服务器测试

    2024年03月21日
    浏览(44)
  • 【Linux】网络---->套接字编程(TCP)

    TCP的编程流程:大致可以分为五个过程,分别是准备过程、连接建立过程、获取新连接过程、消息收发过程和断开过程。 1.准备过程:服务端和客户端需要创建各自的套接字,除此之外服务端还需要绑定自己的地址信息和进行监听。注意:服务端调用listen函数后,处理监听状

    2024年02月04日
    浏览(39)
  • Linux网络编程——tcp套接字

    本章Gitee仓库:tcp套接字 客户端: 客户端: 关于构造和初始化,可以直接在构造的时候,将服务器初始化,那为什么还要写到 init 初始化函数里面呢? 构造尽量简单一点,不要做一些“有风险”的操作。 tcp 是面向连接的,通信之前要建立连接,服务器处于等待连接到来的

    2024年02月20日
    浏览(32)
  • 网络编程套接字之三【TCP】

    目录 1. ServerSocket API(给服务器端使用的类) 2. Socket API(既给服务器使用,也给客户端使用) 3. 写TCP回显—服务器 4. 使用线程池后的TCP服务器代码(最终) 5. 写回显-客户端 6. TCP回显—客户端代码 7. 运行回显服务器和客户端 TCP流套接字编程  ServerSocket 是创建TCP服务端Socket的

    2024年01月19日
    浏览(33)
  • 【Linux网络编程】网络编程套接字(TCP服务器)

    作者:爱写代码的刚子 时间:2024.4.4 前言:本篇博客主要介绍TCP及其服务器编码 只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址 但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换 字符串转in

    2024年04月14日
    浏览(43)
  • TCP/IP网络编程(一) 理解网络编程和套接字

    网络编程和套接字概要 网络编程就是编写程序使两台联网的计算机相互交换数据 为了与远程计算机进行数据传输,需要连接因特网,而编程种的套接字就是用来连接该网络的工具。 构建套接字 1.调用soecket函数创建套接字 2.调用bind函数给套接字分配地址 3.调用listen函数将套

    2024年02月11日
    浏览(41)
  • 【网络通信】socket编程——TCP套接字

    TCP依旧使用代码来熟悉对应的套接字,很多接口都是在udp中使用过的 所以就不会单独把他们拿出来作为标题了,只会把第一次出现的接口作为标题 通过TCP的套接字 ,来把数据交付给对方的应用层,完成双方进程的通信 在 tcpServer.hpp 中,创建一个命名空间 yzq 用于封装 在命名

    2024年02月13日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包