socket编程

目录

一. 

1. ip地址

2. 端口号(port)

3. TCP、UDP协议

二. 使用UDP协议的socket编程

​编辑

1. 日志

2. 常见API

socket

bind

sendto

recvfrom

3. 互相通信服务端

4. 客户端

三. 使用TCP协议的socket编程

1. 常见API

listen

accept

connect

send

recv

2. 服务端

3. 客户端

四. 三次握手、四次挥手


一. 

1. ip地址

在互联网上,所有的设备都分配有一个唯一的公网IP地址。这种地址是用于唯一地标识一台设备连接到互联网上的。而想要进行网络通信,我们需要两种IP地址:源IP地址、目的IP地址。

2. 端口号(port)

端口号作为一个2字节的整数,用于标识一个进程,告诉操作系统,当前数据要交给哪个进程来处理。而在进行网络通信时,只有目的IP地址无法找到对应的程序(进程)来进行通信,因此要配合端口号进行使用。因此,端口号也分为源端口号和目的端口号。

一个端口号只能绑定一个进程,而多个端口号可以绑定同一个进程。

而使用端口号而非进程id是因为能够使得网络通信与进程id直接解耦。

  • 解耦可以更灵活地分配和管理端口号。不再受限于进程ID的变化或冲突,可以更轻松地配置、映射和重新分配端口。
  • 解耦可以支持更高的可扩展性。在传统的模型中,每个进程通常只能使用一个端口号,因此并发连接数量受到限制。而解耦端口号和进程ID后,可以更好地支持并发连接和大规模的系统架构。
  • 解耦可以提供跨平台的兼容性。不同操作系统对进程ID的处理方式可能存在差异,但通过解耦可以确保对于网络通信而言,使用相同的标准来分配和管理端口号,从而实现更好的跨平台兼容性。
  • 解耦可以增加系统的安全性。攻击者无法仅通过获取某个进程的ID来确定该进程在运行时使用的端口号。这种解耦可以降低潜在的安全风险,并增加系统的安全性。
  • 解耦可以简化管理任务。当端口号与进程ID解耦后,网络管理员可以更容易地跟踪和管理分配的端口号,而不需要考虑与进程ID的关联性。这将有助于提高系统的可维护性和管理效率。

3. TCP、UDP协议

TCP(传输控制协议)和UDP(用户数据报协议)是最常见且最常用的传输层协议,它们在网络应用中被广泛使用。

  • TCP是面向连接的协议,提供可靠的、有序的数据传输。在进行数据传输之前,TCP会建立一个连接,保证数据完整性和可靠性。而UDP是面向无连接的协议,它不需要建立连接,也不会验证数据的正常交付,不保证数据传输的可靠性。
  • TCP提供可靠的数据传输,通过使用确认、重传、拥塞控制等机制来确保数据的完整性和正确性。它适用于需要确保数据完整性的应用场景,如文件传输、电子邮件等。相比之下,UDP不提供任何确保数据可靠性的机制,适用于实时性要求高,对数据完整性要求较低的应用,如音频和视频流媒体、DNS查询等。
  • TCP是面向字节流的传输协议,它将应用层提供的数据看作是一连串的字节流,而不是预定义的消息或数据块。这意味着在 TCP 建立连接后,数据被切分成大小不等的数据块去通过网络传输,并在接收端重新组合成完整的数据流。而UDP是面向数据报的传输协议,与TCP的面向字节流不同,UDP将数据划分为数据报(Datagram)。每个数据报都是一个完整的信息单元,具有预定义的大小和边界。


二. 使用UDP协议的socket编程

socket编程_第1张图片

1. 日志

#include 
#include 
#include 
#include 
#include 

#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};


// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    char stdBuffer[1024]; 
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; //自定义部分
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);
    printf("%s%s\n", stdBuffer, logBuffer);
}

2. 常见API

socket

#include 

int socket(int domain, int type, int protocol);

socket函数,用于创建套接字并建立网络连接,为后续的数据传输提供基础支持。socket套接字本质上是一个文件描述符。

domain(协议域):指定套接字所属的协议域,决定了套接字可以用于哪种网络通信。

  • AF_INET:IPv4 网络协议
  • AF_INET6:IPv6 网络协议
  • AF_UNIX /AF_LOCAL:本地 Unix 域套接字

type(套接字类型):指定套接字的类型,决定了套接字的通信方式和性质。

  • SOCK_STREAM:流式套接字,提供基于 TCP 的可靠、面向连接的数据传输
  • SOCK_DGRAM:数据报套接字,提供无连接的不可靠数据传输,基于 UDP
  • SOCK_RAW:原始套接字,用于直接访问网络协议栈
  • SOCK_SEQPACKET:顺序数据包套接字,提供一种可靠的、顺序的、支持带外数据传输的服务

protocol(协议):在某些情况下,可以指定具体的协议,用于进一步细化套接字的行为。通常情况下,当指定了协议域和套接字类型时,协议参数可以设置为 0,表示使用默认的协议。

  • IPPROTO_TCP:TCP 协议
  • IPPROTO_UDP:UDP 协议
  • IPPROTO_SCTP:SCTP 协议

如果调用成功,则返回一个非负整数值表示套接字文件描述符。如果调用失败,则返回-1

_sock = socket(AF_INET, SOCK_DGRAM, 0); 
if (_sock < 0)
{
    logMessage(FATAL, "%d:%s", errno, strerror(errno));
    exit(2);
}

bind

#include 
#include 
#include 
#include 

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

bind函数用于将一个套接字(socket)绑定到一个特定的 IP 地址和端口号,以便可以在该地址和端口上监听连接或进行数据传输。

socket:需要绑定的套接字的文件描述符。

address:指向要绑定的本地 IP 地址和端口号的结构体指针,类型为 struct sockaddr*。对于 IPv4 地址,通常使用 struct sockaddr_in,对于 IPv6 地址,通常使用 struct sockaddr_in6。address_len:address结构体的长度,类型为socklen_t。

如果 调用成功,则返回值为0。如果调用失败,则返回值为-1

socket编程_第2张图片

socket编程_第3张图片

socket编程_第4张图片

以sockaddr_in为例,在我们使用该结构体时,只需要对sin_port和sin_addr进行赋值即可

而内存中的数据存储时有大小端之分,同样,网络数据流也有大小端之分(TCP/UDP都为大端)。因此,在进行网络通信时,我们需要将它们统一。

#include 

//主机转网络
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);

//网络转主机
uint32_t ntohl(uint32_t hostlong);
uint16_t ntohs(uint16_t hostshort);

同时,sin_addr为一字节,以无符号整型存储ip地址。我们也需要从字符串形式与struct in_addr之间进行互相转化。

socket编程_第5张图片

#include 

unsigned long inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);

struct sockaddr_in local;
bzero(&local, 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(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
    logMessage(FATAL, "%d:%s", errno, strerror(errno));
    exit(2);
}
logMessage(NORMAL, "init udp server done ... %s", strerror(errno));

sendto

#include 
#include 

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

sendto函数用于将数据报发送到指定的目标地址

sockfd: 发送端套接字描述符,即要发送数据的套接字。

buf: 存储要发送数据的缓冲区指针。

len: 要发送的数据长度。

flags: 发送操作的标志位,默认为 0,可以使用具体的参数来控制发送行为。

dest_addr: 目标地址结构体指针,指定要发送数据的目标地址和端口。

addrlen: dest_addr的长度。

如果成功发送数据,则返回发送的字节数。如果发生错误,则返回 -1,并设置相应的错误码。

std::string message;
struct sockaddr_in server;
std::getline(std::cin, message);
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);

recvfrom

int recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len);

recvfrom函数用于从套接字接收数据,并且还可以同时获取发送方的地址信息。

sockfd:表示要接收数据的套接字文件描述符。

buf:用于存储接收到的数据的缓冲区。

len:指定缓冲区的大小,即能够接收的最大数据量。

flags:可选参数,用于控制接收操作的行为。

src_addr:输出型参数,用于返回发送方的地址信息,以便于回复或其他处理。

addrlen:输入输出型参数,输入src_addr传入时的长度,输出接受的发送方的长度

如果成功接收到数据,返回值为接收到的数据的长度(以字节为单位)。如果在指定的超时时间内没有接收到数据,并且套接字被设置为非阻塞模式,则返回一个空字符串或者一个特殊的错误代码,如 EAGAIN 或 EWOULDBLOCK。如果发生错误,返回值为 -1,并且可以通过检查全局变量 errno 获取具体的错误信息。常见的错误包括:连接中断、无效的描述符或参数、IO 错误等。

struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
char buffer[1024];
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);

3. 互相通信服务端

netstat -anup 指令:查看udp连接

 我们可以将服务端封装到一个类中

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

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1){}
private:
    uint16_t _port;
    std::string _ip;
    int _sock;
};

首先,我们需要进行socket的创建和bind绑定

bool initServer()
{
    //  1. 创建套接字
    _sock = socket(AF_INET, SOCK_DGRAM, 0); 
    if (_sock < 0)
    {
        logMessage(FATAL, "%d:%s", errno, strerror(errno));
        exit(2);
    }
    // 2. bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
    struct sockaddr_in local;
    bzero(&local, 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(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        logMessage(FATAL, "%d:%s", errno, strerror(errno));
        exit(2);
    }
    logMessage(NORMAL, "init udp server done ... %s", strerror(errno));

    return true;
}

之后,我们就可以进行通信。首先,服务端需要持续进行服务,不需要退出。其次,为了实现多客户端之间的通信,每当一个新的客户端(ip+port)首次进行通信时,将其加入统计,每当一个客户端发送数据,服务端都要进行接受数据并将这些数据发送给所有统计过的客户端。

void Start()
{
    char buffer[SIZE];
    while(true)
    {
        struct sockaddr_in peer;
        bzero(&peer, sizeof(peer));
        socklen_t len = sizeof(peer);
        char key[64];//存储ip+port,用于在哈希表中进行查找、添加
        // start. 读取数据
        ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (s > 0)
        {
            buffer[s] = 0; // 我们目前数据当做字符串
            uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来的!
            std::string cli_ip = inet_ntoa(peer.sin_addr); 
            snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); 
            logMessage(NORMAL, "key: %s", key);
            auto it = _users.find(key);
            if (it == _users.end())//key在哈希表_users中不存在,该key标明的用户端首次进行通信,将其添加到哈希表中
            {
                // exists
                logMessage(NORMAL, "add new user : %s", key);
                _users.insert({key, peer});
            }
        }
            
        //向哈希表所对应的所有用户端发送最新接收到的消息
        for (auto &iter : _users)
        {
            std::string sendMessage = key;
            sendMessage += "# ";
            sendMessage += buffer;
            logMessage(NORMAL, "push message to %s", iter.first.c_str());
            sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
        }
    }

}
private:
    uint16_t _port;
    std::string _ip;
    int _sock;
    std::unordered_map _users;

#include "server.hpp"
#include 
#include 

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

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

    uint16_t port = atoi(argv[1]);
    std::unique_ptr svr(new UdpServer(port));
    svr->initServer();
    svr->Start();
    return 0;
}

4. 客户端

同样,我们需要首先创建socket套接字,但与服务端不同的是,客户端不需要进行bind绑定,这是因为客户端的主要目标是与服务器建立连接,而不是被动地接受连接请求。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "thread.hpp"

uint16_t serverport = 0;
std::string serverip;

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}

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

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    serverport = atoi(argv[2]);
    serverip = argv[1];
}

之后,由于我们需要进行发送自己的数据以及接受服务端从自己和其他客户端所接受到后发送的数据,这也就意味着当我们没有进行发送数据而其他客户端向服务端发送了数据,我们也需要进行接受数据,因此我们可以使用多线程来完成。

#include 
#include 
#include 
#include 

typedef void *(*fun_t)(void *);

class ThreadData
{
public:
    void *_args;
    std::string _name;
};

class Thread
{
public:
    Thread(int num, fun_t callback, void *args) : _func(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
        _name = nameBuffer;

        _tdata._args = args;
        _tdata._name = _name;
    }
    void start()
    {
        pthread_create(&_tid, nullptr, _func, (void*)&_tdata);
    }
    void join()
    {
        pthread_join(_tid, nullptr);
    }
    std::string name()
    {
        return _name;
    }
    ~Thread()
    {
    }

private:
    std::string _name;
    fun_t _func;
    ThreadData _tdata;
    pthread_t _tid;
};
static void *udpSend(void *args)
{
    int sock = *(int *)((ThreadData *)args)->_args;
    std::string name = ((ThreadData *)args)->_name;

    std::string message;
    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());

    while (true)
    {
        std::cerr << "请输入你的信息# "; //标准错误 2打印
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }

    return nullptr;
}

static void *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->_args;
    std::string name = ((ThreadData *)args)->_name;

    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout  << buffer << std::endl;
        }
    }
}

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

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    serverport = atoi(argv[2]);
    serverip = argv[1];

    std::unique_ptr sender(new Thread(1, udpSend, (void *)&sock));
    std::unique_ptr recver(new Thread(2, udpRecv, (void *)&sock));

    sender->start();
    recver->start();

    sender->join();
    recver->join();

    return 0;
}


三. 使用TCP协议的socket编程

socket编程_第6张图片

1. 常见API

listen

#include 
#include 

int listen(int sockfd, int backlog);

listen函数是用于创建一个套接字并开始监听传入连接请求的函数。

sockfd是要监听的套接字的文件描述符。

backlog是指定等待队列的最大长度,即未处理连接请求的最大数量。

如果listen函数成功执行,并且套接字已经成功转换为监听模式,则返回值为0。如果listen函数失败,可能是由于无法将套接字转换为监听模式或指定的套接字无效,则返回值为-1,并且可以使用全局变量errno获取特定的错误代码,以查明失败的原因。

const static int gbacklog = 20; 
if(listen(_sock, gbacklog) < 0)
{
    logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
    exit(4);
} 

accept

#include 
#include 

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept 函数的功能是阻塞等待客户端的连接请求,并返回一个新的套接字描述符,用于与该客户端建立实际的通信连接。这个新的套接字描述符仅与特定的客户端相连,可以用于后续的数据传输。

sockfd:是服务器创建的套接字的文件描述符,用来监听客户端的连接请求。

addr:输出型参数,是一个指向 struct sockaddr 类型的指针,用于保存客户端的地址信息。

addrlen:输入输出型参数,是一个指向 socklen_t 类型的变量的指针,表示传入的addr结构体的长度。

accept 函数成功执行时,返回值为新的套接字描述符。如果出错,则返回-1,并设置errno来指示具体的错误类型。

struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(listensock, (struct sockaddr*)&src, &len);
if(servicesock < 0)
{
    logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
    continue;
}
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);

connect

#include 
#include 

int connect(int socket, const struct sockaddr *address, socklen_t address_len);

connect 函数用于与远程服务器建立连接。

socket:要连接的套接字描述符。

address:指向目标服务器地址的结构体的指针。

address_len:address结构体的长度。

成功连接时,返回0。失败时,返回-1,并将错误代码保存在全局变量errno中供检查。

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());

if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
    std::cerr << "connect error" << std::endl;
    exit(3); // TODO
}

send

#include 
#include 

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

send 函数用于在套接字上发送数据的函数之一。它可以用于通过网络进行数据传输,也可以用于本地通信。类似于udp协议中的sendto

sockfd: 表示需要发送数据的套接字文件描述符。

buf: 指向要发送数据的缓冲区的指针。

len: 表示要发送数据的字节数。

flags: 可选参数,用于控制发送操作的行为。可以使用0作为默认值。

如果成功发送了数据,则返回发送的字节数。发生错误时,返回 -1,并设置相应的错误号(errno)。

recv

#include 
#include 

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

从套接字接收数据的函数,类似于udp协议使用的recvfrom

sockfd: 表示需要接收数据的套接字文件描述符。

buf: 指向接收数据的缓冲区的指针。

len: 表示接收数据的最大字节数。

flags: 可选参数,用于控制接收操作的行为。可以使用 0 作为默认值。

如果成功接收到数据,则返回接收的字节数。当连接被关闭时,返回 0。发生错误时,返回 -1,并设置相应的错误号(errno)。

2. 服务端

netstat -antp 指令:查看tcp连接

同样,我们将服务端进行封装

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


class TcpServer
{
public:
    TcpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), listensock(-1){}

    bool initServer()
    {
    }

    void Start()
    {
    }

    ~TcpServer()
    {
        if (listensock >= 0)
            close(listensock);
    }
private:
    uint16_t _port;
    std::string _ip;
    int listensock;
};

首先,与UDP相同的是,首先都要进行socket套接字的创建和bind绑定,唯一不同的是socket函数的第二个参数从 SOCK_DGRAM (数据报套接字)改为 SOCK_STREAM(流式套接字)。

之后,在进行数据的通信之前,我们还需要进行 listen 监听

private:
    const static int gbacklog = 20; 
public:
bool initServer()
{
    listensock = socket(AF_INET, SOCK_STREAM, 0); 
    if (listensock < 0)
    {
        logMessage(FATAL, "%d:%s", errno, strerror(errno));
        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 = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
    if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        logMessage(FATAL, "%d:%s", errno, strerror(errno));
        exit(2);
    }

    if(listen(listensock, gbacklog) < 0)
    {
        logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
        exit(4);
    }       

    logMessage(NORMAL, "init server success");

    return true;
}

在 listen 监听过后,我们就可以循环式得进行 accept 等待客户端的连接请求后进行信息的通信。

while(true)
{
    struct sockaddr_in src;
    socklen_t len = sizeof(src);
    int servicesock = accept(listensock, (struct sockaddr*)&src, &len);
    if(servicesock < 0)
    {
        logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
        continue;
    }
    uint16_t client_port = ntohs(src.sin_port);
    std::string client_ip = inet_ntoa(src.sin_addr);
    logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",servicesock, client_ip.c_str(), client_port);
}

在使用udp协议时,我们使用到的时 sendto 函数和 recvfrom 函数。而在tcp协议中,除开使用 send 函数和 recv 函数,由于socket套接字本质上就是一个文件描述符,因此我们可以使用文件操作中的 write 和 read 函数进行信息的收发。

而使用udp协议是无连接的网络通信,不能直接使用 write 和 read 函数来发送和接收数据。

在网络通信中,read函数的返回值大于零表示成功读取的字节数,等于零表示对方连接断开,小于零表示 read 函数执行失败。

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << clientip << ":" << clientport << "# " << buffer << std::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));
    }
}

而循环进行 accept 的过程和对其他进行连接好的客户端进行通信的过程是可以同时进行的,因此方法之一是使用多进程(子进程会继承父进程的socket套接字),父进程进行循环 accept ,而子进程进行每个客户端的通信。若是使用这种方式,还需要解决僵尸进程的问题,我们可以使用非阻塞的 wait 函数来进行等待或是通过忽略信号量的方式,当然,我们也可以令子进程再次创建子进程来进行通信,而原本的进程直接退出将进行通信的子进程递交给系统进行等待。

同时我们需要在不同的线程中关闭不需要的socket套接字(父进程关闭 accept 函数所产生的套接字,子进程的子进程关闭 socket 函数所产生的套接字),从而避免文件描述符的泄漏问题。

pid_t id = fork();
if(id == 0)
{
    close(_listensock);
    //子进程创建自己的子进程并退出
    if(fork() > 0) exit(0); 
    //子进程的子进程进行通信
    service(servicesock, client_ip, client_port);
    exit(0);
}
// 父进程
waitpid(id, nullptr, 0); 
close(servicesock);

除此之外,我们还可以使用多线程。同样,我们也需要将不需要的socket套接字关闭,由于线程与线程直接是共享文件描述符,所以我们需要先进行线程分离,之后再从每个线程中删除不必要的socket套接字。

ThreadData *td = new ThreadData();
td->_sock = servicesock;
td->_ip = client_ip;
td->_port = client_port;
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, td);
close(servicesock);
static void *threadRoutine(void *args)
{
    pthread_detach(pthread_self());
    ThreadData *td = static_cast(args);
    service(td->_sock, td->_ip, td->_port);
    delete td;

    return nullptr;
}
class ThreadData
{
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
};

当然我们也可以使用线程池

3. 客户端

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

void usage(std::string proc)
{
    std::cout << "\nUsage: " << 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 = atoi(argv[2]);
    bool alive = false;
    int sock = 0;
    std::string line;
    while (true) // TODO
    {
        if (!alive)
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }
            
            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());

            if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cerr << "connect error" << std::endl;
                exit(3); // TODO
            }
            std::cout << "connect success" << std::endl;
            alive = true;
        }
        std::cout << "请输入# ";
        std::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 << "server 回显# " << buffer << std::endl;
            }
            else if (s == 0)
            {
                alive = false;
                close(sock);
            }
        }
        else
        {
            alive = false;
            close(sock);
        }
    }

    return 0;
}

四. 三次握手、四次挥手

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

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

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