【Linux】socket编程(二)

目录

前言

TCP通信流程

TCP通信的代码实现

tcp_server.hpp编写

tcp_server.cc服务端的编写

tcp_client.cc客户端的编写

整体代码


前言

上一章我们主要讲解了UDP之间的通信,本章我们将来讲述如何使用TCP来进行网络间通信,主要是使用socket API进行代码的实现。

我们一共讲了5个socket API接口,分别为**socket,bind,listen,accept,connect.**但我们在讲解UDP通信时,只使用了socket和bind这两个接口就完成了。而TCP通信会使用后面这三个接口,我们将分别讲解.


TCP通信流程

同样地,TCP通信分为服务器端和客户端,它们的流程分别如下:

服务端通信流程:

  1. 创建套接字 :使用socket函数创建一个套接字,指定协议族为AF_INET(IPv4)或AF_INET6(IPv6),指定类型为SOCK_STREAM(TCP)。

  2. 绑定套接字 :使用bind函数将套接字与服务器的IP地址和端口号绑定在一起。这样服务器将使用指定的IP地址和端口号进行监听。

  3. 监听连接请求 :使用listen函数开始监听连接请求。指定参数backlog,表示允许在队列中等待的最大连接数。

  4. 接受连接请求 :使用accept函数接受客户端的连接请求。该函数会阻塞程序,直到有客户端连接时才返回一个新的套接字 ,用于与客户端进行通信。(新的套接字和旧套接字区别:新套接字负责服务建立的连接,包括通信等,旧套接字则一直负责监听连接.)

  5. 通信 :使用新的套接字进行通信。可以使用readwrite函数进行数据的接收和发送。

  6. 关闭连接 :当通信结束后,使用close函数关闭套接字,释放资源。

客户端通信流程:

  1. 创建套接字 :使用socket函数创建一个套接字,指定协议族为AF_INET(IPv4)或AF_INET6(IPv6),指定类型为SOCK_STREAM(TCP)。

  2. 连接服务器 :使用connect函数连接到服务器的IP地址和端口号。如果连接成功,返回0;否则返回错误码。

  3. 通信 :使用已连接的套接字进行数据的发送和接收,可以使用readwrite函数。

  4. 关闭连接 :当通信结束后,使用close函数关闭套接字,释放资源。


TCP通信的代码实现

依然是三个文件,分别为tcp_server.hpp (用来封装tcp socket),tcp_server.cc (服务器通信代码),tcp_client.cc(客户端通信代码).

tcp_server.hpp编写

首先我们要编写tcp_server.hpp, 首先第一个接口initServer初始化服务端. 一共分为三步:

  • 1.创建套接字

利用socket函数创建新的套接字,并判断是否成功:

复制代码
        listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create sock success,  listensock: %d", listensock);
  • 2.bind绑定

bind将套接字和特定的ip和地址绑定在一起.用法我们上一章也说了,先创建一个sockaddr_in结构体,然后填入相关的数据:sin_family (协议族AF_INET (IPv4)或AF_INET6 (IPv6)),sin_port (端口号),sin_arr.s_addr(ip地址),然后再bind绑定并判断是否成功,代码如下:

复制代码
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if (bind(listensock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind error", errno, strerror(errno));
            exit(3);
        }
  • 3.listen监听

listen监听是否有新的连接,TCP与UDP不同的是,当客户端和服务端正式通信的时候,需要先建立连接,而UDP直接发送数据。所以要listen来监听是否有新链接.

代码如下:

复制代码
        // 3.因为TCP是面向连接的,意味着当我们正式通信的时候,需要先建立连接
        //第二个参数我们在讲TCP协议时会详细讲解,这里先暂且设为20
        if (listen(listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error", errno, strerror(errno));
            exit(3);
        }
        logMessage(NORMAL, "init server success");

第二个接口Start(),该接口主要负责获取连接,并进行通信.共分为两步:

  • accept获取到客户端连接

这个我们同样的需要创建一个sockaddr_in结构体,用来存储客户端的连接信息,然后接收新的套接字,这个套接字是接下来我们通信要使用的。

复制代码
            struct sockaddr_in src;
            socklen_t len = sizeof src;
            //servicesock(未来真正进行IO) vs listensock(主要任务:获取新链接)
            int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error", errno, strerror(errno));
            }
  • 通信流程

这里可以提供两个版本的:一个是单进程版,即每一次只能处理一个客户端.

另一个是 多进程版,通过创建子进程来实现对多个客户端处理.

  • 单进程版

紧接着上面说的,我们获取到客户端的连接信息后,我们需要对其进行解析,得到其ip地址和端口号:

复制代码
            uint16_t client_port = ntohs(src.sin_port);//获得端口号
            string client_ip = inet_ntoa(src.sin_addr);//获得ip
            logMessage(NORMAL, "Link success, %d | %s : %d\n", servicesock,     client_ip.c_str(), client_port);

然后直接执行对应的通信函数即可:

复制代码
 service(servicesock,client_ip,client_port);
  • 多进程版:

利用fork函数实现,代码如下:后面的服务端通信和客户端通信都不用改动

复制代码
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                //子进程
                close(listensock);
                service(servicesock,client_ip,client_port);
                exit(0);//僵尸状态
            }
            close(servicesock);

通信函数service的实现:我们从sock中读取消息,客户端没有发消息时,服务端会阻塞在这里等待用户的输入。

复制代码
static void service(int sock,const string& clientip,const uint16_t& clientport)
{
    //echo server
    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    while(true)
    {
        //read && write
        ssize_t s = read(sock,buffer,sizeof buffer-1);
        if(s > 0)
        {
            buffer[s] = 0;//将发过来的数据当做字符串
            cout << clientip << " : " << clientport << "# "<< buffer << endl;
        }
        else if(s== 0)//对端链接关闭
        {
            logMessage(NORMAL,"%s : %d shutdown, me too!",clientip.c_str(),clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock,buffer,strlen(buffer));
    }
    close(sock);
}

tcp_server.cc服务端的编写

这个就很简单了,只需要调用initServer初始化和Start开始就行了.

复制代码
#include "tcp_server.hpp"
#include <memory>

static void usage(string proc)
{
    cout << "Usage: " << proc << "ServerPort\n" << endl;
}

//./tcp_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<TcpServer> svr(new TcpServer(port));

    svr->initServer();
    svr->Start();

    return 0;
}

tcp_client.cc客户端的编写

  • 创建套接字:

    复制代码
      int sock = socket(AF_INET, SOCK_STREAM, 0);
  • 调用connect与服务端链接:利用命令行参数,将用户输入的ip地址和port端口号获取到,然后传入sockaddr_in结构体,最后进行connect

    复制代码
      uint16_t serverPort = atoi(argv[2]);
      string serverIp = argv[1];    
    
      struct sockaddr_in server;
      bzero(&server, sizeof server);
      server.sin_family = AF_INET;
      server.sin_port = htons(serverPort);
      server.sin_addr.s_addr = inet_addr(serverIp.c_str());
      if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
  • 进行通信(send和recv)

TCP的发送和接收消息不同于UDP的sendto和recvfrom,而是send和recv。我们分别看一下函数的用法:

send:

复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd:发送数据的套接字描述符。即想谁发送
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送的数据的长度(以字节为单位)。
  • flags:附加选项,通常设为0。
  • 作用:send()函数用于将数据从发送端发送到接收端。它返回已发送的字节数,或者在出现错误时返回-1 。可以通过设置flags参数来指定传输数据的特定选项,例如设置为MSG_DONTWAIT非阻塞发送等。

recv:

复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:要接收数据的套接字描述符。即谁接收
  • buf:接收数据的缓冲区的指针。
  • len:接收数据的最大长度(以字节为单位)。
  • flags:附加选项,通常设为0。
  • 作用:recv()函数用于从套接字接收数据,并将其存储在指定的缓冲区中。它返回接收到的字节数,或者在出现错误时返回-1。 可以通过设置flags参数来指定接收数据的特定选项,例如设置为MSG_DONTWAIT非阻塞接收等。

所以通信代码如下:

复制代码
    while (true)
    {
        string line;
        cout << "Please Enter Message# ";
        getline(cin, line);
        send(sock, line.c_str(), line.size(), 0);
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << "server echo# " << buffer << endl;
        }
        else if (s == 0)
        {
            break;
        }
        else
        {
            break;
        }
    }

至此我们的TCP通信就完成了.

当我们使用多进程通信时,可以有多个客户端同时向服务端发送消息:

至此,TCP的网络通信流程也完成了,这是完整的代码,可以直接 拷贝运行,可去掉logMessage相关的调试信息.

整体代码

注意运行服务器时,使用**./tcp_server 端口号**

运行客户端连接服务器时,使用**./tcp_clinet 服务器ip 服务器端口号**

tcp_server.hpp文件

复制代码
#pragma once
#include <iostream>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <memory>
#include <pthread.h>
#include <signal.h>
#include <cstring>
#include <ctype.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;

static void service(int sock,const string& clientip,const uint16_t& clientport)
{
    //echo server
    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    while(true)
    {
        //read && write
        ssize_t s = read(sock,buffer,sizeof buffer-1);
        if(s > 0)
        {
            buffer[s] = 0;//将发过来的数据当做字符串
            cout << clientip << " : " << clientport << "# "<< buffer << endl;
        }
        else if(s== 0)//对端链接关闭
        {
            logMessage(NORMAL,"%s : %d shutdown, me too!",clientip.c_str(),clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock,buffer,strlen(buffer));
    }
}

class TcpServer
{
public:
    const static int gbacklog = 20;

    TcpServer(uint16_t port, string ip = "")
        : _port(port), _ip(ip), listensock(-1)
    {
    }
    void initServer()
    {
        // 1.创建套接字
        listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create sock success,  listensock: %d", listensock);
        // 2.bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if (bind(listensock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind error", errno, strerror(errno));
            exit(3);
        }
        // 3.因为TCP是面向连接的,意味着当我们正式通信的时候,需要先建立连接
        if (listen(listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error", errno, strerror(errno));
            exit(3);
        }
        logMessage(NORMAL, "init server success");
    }
    void Start()
    {
        //version2 :signal(SIGCHLD,SIG_IGN); //对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸进程
        while (true)
        {
            // sleep(1);
            // 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof src;
            // sock(未来真正进行IO) and _sock(主要任务:获取新链接)
            int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error", errno, strerror(errno));
            }
            // 获取连接成功
            uint16_t client_port = ntohs(src.sin_port);
            string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "Link success, %d | %s : %d\n", servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务
           
            // version 1 -- 单进程循环 -- 只能一次处理一个客户端,处理完一个,才能处理下一个
            // 显然是不能被直接使用的?为什么?单进程.
            service(servicesock,client_ip,client_port);
            // version 2 -- 多进程版本 -- 创建子进程,
            // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? 答案是当然可以!
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                //子进程
                close(listensock);
                service(servicesock,client_ip,client_port);
                exit(0);//僵尸状态
            }
            //父进程
            close(servicesock);
        }
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    string _ip;
    int listensock;
    unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

tcp_server.cc文件

复制代码
#include "tcp_server.hpp"
#include <memory>

static void usage(string proc)
{
    cout << "Usage: " << proc << "ServerPort\n" << endl;
}

//./tcp_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<TcpServer> svr(new TcpServer(port));

    svr->initServer();
    svr->Start();

    return 0;
}

cline.cc文件

复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <strings.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
static void usage(string proc)
{
    cout << "Usage: " << proc << "ServerIP ServerPort" << endl;
}
// ./tcp_clinet IP Prot
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(-1);
    }
    uint16_t serverPort = atoi(argv[2]);
    string serverIp = argv[1];

    int sock = socket(AF_INET, SOCK_STREAM, 0);

    if (sock < 0)
    {
        cerr << "sokcet error" << endl;
        exit(2);
    }
    // client 不需要显式的bind,OS会自动选择
    // 更不需要监听,但是需要连接的能力connect
    struct sockaddr_in server;
    bzero(&server, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    server.sin_addr.s_addr = inet_addr(serverIp.c_str());
    if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
    {
        cerr << "connect error" << endl;
        exit(3);
    }
    cout << "connect success!" << endl;

    while (true)
    {
        string line;
        cout << "Please Enter Message# ";
        getline(cin, line);
        send(sock, line.c_str(), line.size(), 0);
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << "server echo# " << buffer << endl;
        }
        else if (s == 0)
        {
            break;
        }
        else
        {
            break;
        }
    }
    close(sock);
    return 0;
}
相关推荐
秦jh_1 分钟前
【Qt】常用控件(上)
服务器·数据库·qt
晨曦夜月5 分钟前
头文件与目标文件的关系
linux·开发语言·c++
Xyz996_5 分钟前
Ansible进行Nginx编译安装的详细步骤
运维·ansible
白仑色7 分钟前
java中的anyMatch和allMatch方法
java·linux·windows·anymatch·allmatch
狂奔的sherry9 分钟前
WIFI后端功能问题解决
网络
云和数据.ChenGuang10 分钟前
自动化运维工程师之ansible启动rpcbind和nfs服务
运维·服务器·运维技术·数据库运维工程师·运维教程
yimengsama11 分钟前
VMWare虚拟机如何连接U盘
linux·运维·服务器·网络·windows·经验分享·远程工作
松涛和鸣15 分钟前
32、Linux线程编程
linux·运维·服务器·c语言·开发语言·windows
云和数据.ChenGuang17 分钟前
AB压力测试运维工程师技术教程
运维·压力测试·运维工程师
꧁坚持很酷꧂18 分钟前
把虚拟机Ubuntu中的USB设备名称改为固定名称
linux·数据库·ubuntu