linux socket编程之tcp(实现客户端和服务端消息的发送和接收)

目录

一.创建socket套接字(服务器端)

二.bind将port与端口号进行绑定(服务器端)

2.1填充sockaddr_in结构

2.2bind绑定端口

三.建立连接

四.获取连接

五..进行通信(服务器端)

5.1接收客户端发送的消息

5.2给客户端发送消息

5.3引入多线程

六.客户端通信

6.1创建socket套接字

6.2客户端bind问题

6.3建立连接

6.4进行通信

6.4.1.给服务器发送消息

6.4.2.接受服务器发送的消息

七.效果展示

八.代码展示

 6.1TcpServer.hpp

6.2TcpClient.cc

6.3InetAddr.hpp

6.4LockGuard.hpp

6.5Log.hpp

6.6main.cc

6.7makefile


一.创建socket套接字(服务器端)

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

domain:选择你要使用的网络层协议 一般是ipv4,也就是AF_INET

type:选择你要使用的应用层协议,这里我们选择tcp,也就是SOCK_STREAM

protocol:这里我们先设置成0

成功返回文件描述符,失败返回-1

_listen_sockfd = socket(AF_INET,SOCK_STREAM,0);
if (_listen_sockfd < 0)
{
    LOG(FATAL, "socket error");
    exit(SOCKET_ERROR);
}
LOG(DEBUG, "socket create success, sockfd is : %d\n", _listen_sockfd);

二.bind将port与端口号进行绑定(服务器端)

2.1填充sockaddr_in结构

uint16_t htons(uint16_t hostshort);//将端口号从主机序列转成网络序列
in_addr_t inet_addr(const char *cp);//将ip从主机序列转成网络序列 + 字符串风格ip转成点分十进制ip
uint16_t ntohs(uint16_t netshort);//将端口号从网络序列转成主机序列
char *inet_ntoa(struct in_addr in);//将ip从网络序列转成主机序列 + 点分十进制ip转成字符串风格ip

网络通信:struct sockaddr_in

本地通信:sockaddr_un

16位地址类型表明了他们是网络通信还是本地通信 

16位地址类型:sin_family

16位端口号:sin_port

32位ip地址:sin_addr.s_addr

//填充sockaddr_in结构
struct sockaddr_in local;
local.sin_family = AF_INET;//表明是网络通信
local.sin_port = htons(_port);//将主机序列转成网络序列
local.sin_addr.s_addr = inet_addr("0.0.0.0");//将字符串类型的点分十进制ip转成四字节ip,并转成网络序列

2.2bind绑定端口

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

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

struct sockaddr *:包含ip地址+端口号的结构体(类型不一样需要进行强转)

socklen_t addrlen:sockaddr_in结构体的大小

//bind绑定端口
int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
    LOG(FATAL, "bind error");
    exit(BIND_ERROR);
}
LOG(DEBUG, "bind success, sockfd is : %d\n", _listen_sockfd);

三.建立连接

#include <sys/types.h>       
#include <sys/socket.h>
int listen(int sockfd, int backlog);

sockfd:要建立连接的socket套接字的文件描述符

backlog: 这个值我们暂时先设置成16,他表示能建立的最多的连接数量

//3.建立连接    tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的
int m = listen(_listen_sockfd, default_backlog);//default_backlog = 16
if (m < 0)
{
    LOG(FATAL, "listen error");
    exit(_listen_sockfd);
}
LOG(DEBUG, "listen success, sockfd is : %d\n", _listen_sockfd);

四.获取连接

在直接通信之前,我们需要先获取连接

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

sockfd:从哪个listen套接字获取连接

addr:数据来源于哪个客户端

addrlen:addr的类型大小

// 4.获取连接 不能直接接受数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listen_sockfd, (sockaddr *)&peer, &len);
if (sockfd < 0)
{
    LOG(WARNING, "accept error\n");
    continue;
}

五..进行通信(服务器端)

5.1接收客户端发送的消息

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

sockfd:获取连接时得到的socket套接字的文件描述符

buf:缓存区

len:缓存区的大小,单位是字节

flags:暂时设置为0

//5.1接受客户端发送的消息
char inbuffer[1024];
ssize_t n = recv(sockfd, inbuffer, sizeof(inbuffer) - 1,0);

5.2给客户端发送消息

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

sockfd:套接字的文件描述符

buf:缓存区

len:缓存区的大小,单位是字节

flags:设置为0

//5.2给客户端发送消息
send(sockfd, echo_string.c_str(), echo_string.size(),0);

5.3引入多线程

class ThreadData
{
public:
    ThreadData(int fd, InetAddr addr, TcpServer *s):sockfd(fd), clientaddr(addr), self(s)
    {}
public:
    int sockfd;
    InetAddr clientaddr;
    TcpServer *self;
};

static void *HandlerSock(void *args)
{
    pthread_detach(pthread_self());
    ThreadData *td = static_cast<ThreadData *>(args);
    td->self->Service(td->sockfd, td->clientaddr);
    delete td;
    return nullptr;
}


//采用多线程
pthread_t t;
ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);
pthread_create(&t, nullptr, HandlerSock, td); //将线程分离

六.客户端通信

6.1创建socket套接字

//1.创建socket套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

6.2客户端bind问题

客户端不需要显示的bind,os会自动帮你绑定端口号!!!

试想一下,你的手机上有抖音和微信两个客户端小程序,如果抖音客户端bind了8080这个端口,微信也想要bind 8888这个端口,那么这时候就会出现一个问题,一个端口号被两个进程竞争!!!结果就是,抖音和微信不可能同时启动。

所以解决方法就是:tcp client建立连接的时候,OS会自己自动随机的给client进行bind

6.3建立连接

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

sockfd:socket套接字的文件描述符

addr:携带客户端信息的结构体

addrlen:结构体的大小

    //2.建立连接
    struct sockaddr_in server;
    // 构建目标主机的socket信息
    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());

    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));

6.4进行通信

6.4.1.给服务器发送消息

//3.给服务器发送消息
ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0);

6.4.2.接受服务器发送的消息

//接受服务器发送的消息
ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer)-1, 0);
if(m > 0)
{
    inbuffer[m] = 0;
    std::cout << inbuffer<< std::endl;
}

七.效果展示

八.代码展示

 6.1TcpServer.hpp

用来进行tcp服务端通信

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>

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

enum
{
    SOCKET_ERROR = 1, // 创建套接字失败
    BIND_ERROR,       // bind绑定端口失败
    _listen_sockfd,   //创建listen_sockfd失败
    USAGE_ERROR       // 启动udp服务失败
};

int default_listen_sockfd = -1;
int default_backlog = 16;
class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, InetAddr addr, TcpServer *s):sockfd(fd), clientaddr(addr), self(s)
    {}
public:
    int sockfd;
    InetAddr clientaddr;
    TcpServer *self;
};

class TcpServer
{
public:
    TcpServer(uint16_t port) : _port(port), _listen_sockfd(default_listen_sockfd)
    {
    }
    void Init()
    {
        // 1.创建socket套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            LOG(FATAL, "socket error");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, sockfd is : %d\n", _listen_sockfd);

        // 2.bind将套接字和端口号进行绑定
        // 填充sockaddr_in结构
        struct sockaddr_in local;
        local.sin_family = AF_INET;                   // 表明是网络通信
        local.sin_port = htons(_port);                // 将主机序列转成网络序列
        local.sin_addr.s_addr = inet_addr("0.0.0.0"); // 将字符串类型的点分十进制ip转成四字节ip,并转成网络序列

        // bind绑定端口
        int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "bind success, sockfd is : %d\n", _listen_sockfd);

        // 3.建立连接    tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的
        int m = listen(_listen_sockfd, default_backlog); // default_backlog = 16
        if (m < 0)
        {
            LOG(FATAL, "listen error");
            exit(_listen_sockfd);
        }
        LOG(DEBUG, "listen success, sockfd is : %d\n", _listen_sockfd);

    }

    void Service(int sockfd, InetAddr client)
    {
        LOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);

        std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]# ";
        while (true)
        {
            char inbuffer[1024];
            ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << clientaddr << inbuffer << std::endl;

                std::string echo_string = "[server echo]# ";
                echo_string += inbuffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                // client 退出&&关闭连接了
                LOG(INFO, "%s quit\n", clientaddr.c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error\n", clientaddr.c_str());
                break;
            }
        }
        std::cout << "server开始退出" << std::endl;
        sleep(10);
        close(sockfd); // 文件描述符泄漏
    }
    static void *HandlerSock(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->self->Service(td->sockfd, td->clientaddr);
        delete td;
        return nullptr;
    }

    void Stat()
    {
        while (true)
        {
            // 4.获取连接 不能直接接受数据
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;
            }
            //采用多线程
            pthread_t t;
            ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);
            pthread_create(&t, nullptr, HandlerSock, td); //将线程分离
        }
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listen_sockfd;
};

6.2TcpClient.cc

用来进行tcp客户端通信

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

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

// ./tcp_client serverip serverport
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]);

    //1.创建socket套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // tcp client 要bind,不要显示的bind.
    //2.建立连接
    struct sockaddr_in server;
    // 构建目标主机的socket信息
    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());

    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }

    while(true)
    {
        std::cout << "Please Enter# ";
        std::string outstring;
        std::getline(std::cin, outstring);
        //3.给服务器发送消息
        ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0); //write
        if(s > 0)
        {
            char inbuffer[1024];
            //接受服务器发送的消息
            ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer)-1, 0);
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::cout << inbuffer<< std::endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }

    close(sockfd);//防止文件描述符泄漏
    return 0;
}

6.3InetAddr.hpp

用来解析request中包含的对方主机的ip地址和prot端口号

#pragma once

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

class InetAddr
{
private:
    void GetAddress(std::string *ip, uint16_t *port)
    {
        *port = ntohs(_addr.sin_port);
        *ip = inet_ntoa(_addr.sin_addr);
    }

public:
    InetAddr(const struct sockaddr_in &addr) : _addr(addr)
    {
        GetAddress(&_ip, &_port);
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

6.4LockGuard.hpp

用来对日志信息进行加锁操作

#include <iostream>
#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex); // 构造加锁
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);// 析构释放锁
    }
private:
    pthread_mutex_t *_mutex;
};

6.5Log.hpp

用来打印日志信息

#pragma once
#include <cstdio>
#include <iostream>
#include <string>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
#include <fstream>

#include "LockGuard.hpp"


bool IsSave = false;//是否向文件中写入
const std::string logname = "log.txt";//日志信息写入的文件路径

// 日志是有等级的
enum Level
{
    DEBUG = 0,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

// 将日志的登记由整形转换为字符串
std::string LevelToString(int level)
{
    switch (level)
    {
    case DEBUG:
        return "Debug";
    case INFO:
        return "Info";
    case WARNING:
        return "Warning";
    case ERROR:
        return "Error";
    case FATAL:
        return "Fatal";
    default:
        return "Unknown";
    }
}

// 获取时间
std::string GetTimeString()
{
    time_t curr_time = time(nullptr);
    struct tm *format_time = localtime(&curr_time);
    if (format_time == nullptr)
        return "None"; // 没有获取成功

    char time_buffer[1024];
    snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
             format_time->tm_year + 1900, // 这里的year是减去1900之后的值,需要加上1900
             format_time->tm_mon + 1,     // 这里的mon是介于0-11之间的,需要加上1
             format_time->tm_mday,
             format_time->tm_hour,
             format_time->tm_min,
             format_time->tm_sec);
    return time_buffer;
}


//将日志信息写入到文件中
void SaveFile(const std::string &filename, const std::string &message)
{
    std::ofstream out(filename, std::ios::app);
    if (!out.is_open())
    {
        return;
    }
    out << message;
    out.close();
}

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//定义锁 支持多线程
// 日志是有格式的
// 日志等级 时间 代码所在的文件名/行数 日志的内容
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{

    std::string levelstr = LevelToString(level);
    std::string timestr = GetTimeString();
    pid_t selfid = getpid();

    char buffer[1024];

    va_list arg;//定义一个void* 指针
    va_start(arg, format);//初始化指针,将指针指向可变参数列表开始的位置
    vsnprintf(buffer, sizeof(buffer), format, arg);//将可变参数列表写入到buffer中
    va_end(arg);//将指针置空

    std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +
                          "[" + std::to_string(selfid) + "]" +
                          "[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer + "\n";

    LockGuard lockguard(&lock);
    if (!issave)
    {
        std::cout << message;//将日志信息打印到显示器中
    }
    else
    {
        SaveFile(logname, message);//将日志信息写入到文件
    }
}

// C99新特性__VA_ARGS__
#define LOG(level, format, ...)  do{ LogMessage(__FILE__, __LINE__,IsSave,level, format, ##__VA_ARGS__); }while(0)
#define EnableFile()    do{ IsSave = true; }while(0)
#define EnableScreen()  do{ IsSave = false; }while(0) 

6.6main.cc

主函数

#include <iostream>
#include <memory>
#include "TcpServer.hpp"

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;
}

// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    EnableScreen();
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> usvr = std::make_unique<TcpServer>(port);

    usvr->Init();

    usvr->Stat();
    return 0;
}

6.7makefile

.PHONY:all
all:tcpserver tcpclient
tcpclient:TcpClient.cc
	g++ -o tcpclient TcpClient.cc -std=c++14 
tcpserver:main.cc
	g++ -o tcpserver main.cc -std=c++14 -lpthread
.PHONY:clean
clean:
	rm -f tcpserver tcpclient
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值