首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Linux网络套接字

Linux网络套接字

作者头像
有礼貌的灰绅士
发布2025-04-08 08:11:37
发布2025-04-08 08:11:37
36000
代码可运行
举报
运行总次数:0
代码可运行

Socket 编程 UDP

本章将函数介绍和代码编写实战一起使用。 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr.sin_addr 表示 32 位 的 IP 地址 但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和in_addr 表示之间转换; 字符串转 in_addr 的函数:

代码语言:javascript
代码运行次数:0
运行
复制
#include <arpa/inet.h>
int inet_aton(const char:*strptr,struct inaddr *addrptr);
int_addr_t inet_addr(const char *strptr);
int inet_pton(int family,const char *strptr,void *addrptr);

in_addr 转字符串的函数:

代码语言:javascript
代码运行次数:0
运行
复制
char *inet_ntoa(struct in addrinaddr);
const char *inet_ntop(int family,const void *addrptr, char *strptr,size t len);

查看OS所有UDP和进程信息:

netstat -naup

这个函数是创建一个套接字:

int socket(int domain, int type,int protocol); 第一个参数是套接字的域,就是确定是ipv4还是ipv6等待。 第二个是套接字的数据流类型。 第三个参数是协议类型。 返回值是:返回成功返回一个文件操作符,失败返回-1。 其实socket也就相当于创建了一个文件。

这个函数是绑定套接字:

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 第一个参数是网络文件描述符 第二个参数是用哪一种套接字(让绑定过来的套接字实现哪一种功能)

将指定内存全部初始化为0的函数:

void bzero(void *s, size_t n); 第一个参数是传地址 第二个参数是缓冲区的大小

in_addr_t inet_addr(const char *cp); 让字符串转换成网络ip风格的四字节

接收信息函数:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); 第二个参数为接收容器 第三个参数为信息长度 第四个参数为设置阻塞与非阻塞 第六个参数为套接字种类的长度

发送信息函数:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); 参数和上一个函数差不多,只有最后一个参数是不需要取地址的

首先有一个代码的预备工作,实现一个日志附带文件打印功能的代码

代码语言:javascript
代码运行次数:0
运行
复制
//log.hpp
#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log()
    {
    }
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};
Log log;

模拟服务器

代码语言:javascript
代码运行次数:0
运行
复制
#include "udpserver.hpp"
#pragma once
#include <memory>
#include <cstring>
#include <sys/types.h>          
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

extern Log log;//声明变量log
enum{
    SOCKET_ERR = 1,//套接字创建失败
    BIND_ERR
};

uint16_t defaultport = 8080;//默认端口
//绑定端口号的时候要注意,很多端口都是被应用层协议固定使用[0,1023]这个区间:http:80 https:443等等
//建议使用1024以上,但也要注意,比如mysql:3306
string defaultip = "0.0.0.0";//默认ip,bind为0就是任意ip地址都可以进到服务器来

const int size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t &port = defaultport, const 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)
        {
            log(Fatal,"socket create error, socket:%d",sockfd_);
            exit(SOCKET_ERR);
        }
        log(Info,"socket create success, socket:%d",sockfd_);
        //2.bind socket
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;//设置自己为IPV4
        local.sin_port = htons(port_);//因为端口号需要给对方发送,所以必须要保证我的端口号是网络字节序列
        local.sin_addr.s_addr = inet_addr(ip_.c_str());
        //1.将string->uint32_t 2.必须是网络序列
        //sin_addr里面还有一个成员,s_addr才是真实的本体 
        //local.sin_addr.s_addr=INADDR_ANY;也是绑定任意IP地址的方法
        int n = bind(sockfd_,(const struct sockaddr *)&local,sizeof(local));
        if(n < 0)
        {
            log(Fatal, "bind error, error: %d, err string:%s",errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info,"bind success, errno: %d, err string: %s",errno,strerror(errno));

    }
    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)
            {
                log(Warning,"recvfrom error,errno: %d,err string:%s",errno,strerror(errno));
                continue;
            }
            string info = inbuffer;
            string echo_string = "server echo#" + info;
            sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0) close(sockfd_); 
    }
private:
    int sockfd_;//网络文件描述符
    string ip_;//服务器iP地址
    uint16_t port_;//服务器进程的端口号
    bool isrunning_;//服务器是否在运行
};
代码语言:javascript
代码运行次数:0
运行
复制
#include "log.hpp"
#include "udpserver.hpp"

void Usage(string proc)
{
    cout << "\n\rUsage:" << proc << "port[1024+]\n" << endl;
}
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = stoi(argv[1]);
    unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run();
    return 0;
}

客户端

代码语言:javascript
代码运行次数:0
运行
复制
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>          
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
using namespace std;

void Usage(string proc)
{
    cout << "\n\rUsage:" << proc << "port[1024+]\n" << endl;
}
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 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());
    socklen_t len = sizeof(server);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0)
    {
        cout << "socket error" << endl;
        exit(1);
    }
    //客户端需要绑定,但是不需要显示绑定,由OS自己选择
    //为了防止进程端口出现冲突
    //OS什么时候给我绑定的呢?是在首次发送数据的时候
    string message;
    char buffer[1024];
    while(true)
    {
        cout << "Please Enter@ ";
        getline(cin,message);
      
        //1.数据2.发给谁
        
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);
        
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len);
        if(s > 0)
        {
            buffer[s] = 0;
            cout << buffer <<endl;
        }
    }
    close(sockfd);
    return 0;
}

Socket 编程 TCP

测试服务器工具,指定服务器远程登陆:

telnet 127.0.0.1 端口号 127.0.0.1表示本地环回。

第一个函数也是将字符串的ip转换成网络四字节的ip。

因为TCP是面向连接的,服务器比较被动,一直处于等待链接到来的状态,所以用监听的方式查看是否有客户端到来。 这个函数是将套接字设置监听状态:

int listen(int sockfd, int backlog); 第二个参数等后面TCP协议在进行解释

接收消息函数,并获知哪个客户端连接上了自己:

int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags); 这里返回值也是一个套接字,那么我们第一个参数也是套接字,有什么区别呢? 我们输入的套接字就相当于饭店外面的接待员,并不参与真正的服务当中,返回值的套接字才是真正的服务员,服务被接待过的客人。 也就是说,第一个参数的套接字仅仅是帮助我们获取新连接的工具人。

通过指定的套接字发送连接。

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

这个客户端与服务器的程序还是要用到上面log的代码: 服务器

代码语言:javascript
代码运行次数:0
运行
复制
#pragma once
#include "log.hpp"
#include <memory>
#include <sys/socket.h>
#include <cstdlib>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
extern Log log;
const int defaultfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10;
enum 
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError
};
class TcpServer;
class ThreadData
{
public:
    ThreadData(int fd, const string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};
class TcpServer
{
public:
    TcpServer(const uint16_t &port,const string &ip = defaultip):listensock_(defaultfd),port_(port),ip_(ip)
    {

    }
    void InitServer()
    {
        listensock_ = socket(AF_INET,SOCK_STREAM,0);
        if(listensock_ < 0)
        {
            log(Fatal,"create socket,errno: %d,errstring: %s",errno,strerror(errno));
            exit(SocketError);
        }
        log(Info,"create socket success, sockfd:%d",listensock_);
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(),&(local.sin_addr));
        if(bind(listensock_,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
            log(Fatal,"bind error, errno: %d, errstring:%s",errno,strerror(errno));
            exit(BindError);
        }
        if(listen(listensock_,backlog)<0)
        {
            log(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }
        log(Info, "listen socket success, listensock_: %d", listensock_);
    }
    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());//分离状态不用让主线程去等待,互不影响
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
        delete td;
        return nullptr;
    }
    void Start()
    {
        log(Info, "tcpServer is running....");
        for(;;)
        {
            //1.获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_,(struct sockaddr*)&client,&len);
            if(sockfd < 0)
            {
                log(Warning, "accept error, 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));
            //2. 根据新连接来进行通信
            log(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
            //单进程版本,无法让多个客户端进行连接
            /*Service(sockfd, clientip,clientport);
            close(sockfd);*/
            //多进程版,让子进程去处理客户端,父进程继续向下执行。创建多个进程就能连接多个客户端
            /*pid_t id = fork();
            if(id == 0)
            {
                //child
                close(listensock_);//这个是父进程用的fd,防止误操作
                if(fork() > 0) exit(0);//因为是阻塞等待,所以让子进程再创建子进程去处理客户端
                Service(sockfd, clientip, clientport); //孙子进程来处理客户端,system 领养
                close(sockfd);
                exit(0);
            }
            close(sockfd);//这里必须关闭,因为sockfd已经交给子进程处理了,父进程不需要在管理了,不然父进程的文件描述符会越用越少
            // father
            pid_t rid = waitpid(id, nullptr, 0);
            (void)rid;*/
            //多线程版
            ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);
        }
    }
    void Service(int sockfd,const string& clientip,const uint16_t &clientport)//因为是面向字节流的,所以用read和write对网络文件进行读写即可
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                cout << "client say# " << buffer << endl;
                string echo_string = "tcpserver echo# ";
                echo_string += buffer;
    
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)//客户端退出,就会关闭套接字
            {
                log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                log(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }
    ~TcpServer()
    {

    }
private:
    int listensock_;
    string ip_;
    uint16_t port_;
};
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    //TCP方式的客户端bind实在什么时候呢?
    //是在connect的时候OS自动绑定
    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));
    int sockfd = 0;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        cerr << "socket error" << endl;
        return 1;
    }
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        cerr << "connect error..., reconnect: "<< endl;
    }
    while(true)
    {
        string message;
        cout << "Please Enter# ";
        getline(cin, message);
        int n = write(sockfd, message.c_str(), message.size());
        if (n < 0)
        {
            cerr << "write error..." << endl;
            
        }
        char inbuffer[4096];
        n = read(sockfd, inbuffer, sizeof(inbuffer));
        if (n > 0)
        {
            inbuffer[n] = 0;
            cout << inbuffer << endl;
        }
    }
    close(sockfd);
    return 0;
}

客户端

代码语言:javascript
代码运行次数:0
运行
复制
#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]);
    std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
    tcp_svr->InitServer();
    tcp_svr->Start();
    return 0;
}

守护进程

两个人在使用一个服务器,那么服务器就会生成两个“会话”(session),会话里面包含了命令行解释器(bash),和多个进程。

眼下,两个bash都是前台进程,其他的都是后台进程,并且一个会话当中只能有一个前台进程。 无论前台还是后台进程都可以向显示器进行打印,但是能用键盘的只有前台。(比如说将bash变成后台,另一个进程变成前台进程,Ctrl+C就能让这个进程停止,然后将bash换回到前台)——谁拥有键盘文件谁就是前台。

如何将进程后台运行呢? 只需要在启动可执行文件的时候加上一个&即可。 如果想让进程变成前台需要

fg+任务号

用jobs指令来查看后台任务号。

如果某一个前台进程被暂停之后放到后台了,想让这个后台继续运行:

bg+任务号

进程间的关系

这里的PGID是进程组的ID,SID是会话的ID。 会话也是要被OS组织管理的。 第一个进程PID和PGID是一样的,说明这个进程自成一组。 剩下三个PGID一样说明他们三个是一组。(一般来说,第一个进程是组长) 组长是组内中PID和PGID相同的进程。

平时所有任务其实都是进程组在完成! 也就是说,前台进程和后台进程其实并不正确,应该叫做前台任务和后台任务!

那么SID的ID是谁呢? 其实就是bash。

如果客户端退出了呢?

TTY全变成了?也就是说跟终端无关了。 TPGID变成了1。 并且退出的客户端的进程全都被OS给领养了。 也就是说这些进程收到了客户端登录和退出的影响。

如果不想让这些进程受到客户端的影响,那么这就叫守护进程化。(也叫精灵进程)

注销 windowsOS党总有一个操作叫做注销,注销就是将整个会话给关闭。 当重新登录的时候就相当于重新创建一个会话。

守护进程 如何进行守护进程化呢? 那就是让一个会话当中的某个进程脱离当前会话,自成一个会话,上一个会话进行销毁也就和这个进程无关了。

函数接口

#include <unistd.h> pid_t setsid(void); 谁调用这个函数谁就被设置成为守护进程,成功返回一个新的pid,失败返回-1.

但是这个函数不会让这个进程成为新会话的组长。 可是新的会话只有这个进程,那怎么办呢?只要不让这个进程是第一个进程就好了。

if(fork()>0) exit(0); srtsid();

所以守护进程的本质也是孤儿进程。

如果程序生成一个守护进程(以服务器举例),分为以下几个步骤:

1.忽略部分异常信号 2.将自己变成独立会话 3.更改当前调用进程的工作目录 4.标准输入输出错误不要在打印到屏幕上,重定向到/dev/null(也可以放在一个文件里形成文件的日志)

这样就能让一个服务器在后台持久运行了。 注意:守护进程命名习惯是后面以d为结尾。

让进程和以上效果相同的函数: 第一个参数是设置为0是将工作目录设置为根目录,否则就是当前目录, 第二个参数是设置为0是将标准输入输出错误重定向到/dev/null。

TCP简单的特性

三次握手与四次挥手 TCP会三次握手来进行链接的建立:

通过四次挥手进行释放:

注意:TCP是全双工的。(可以互相通信) 那么为什么不会相互收到影响呢?

因为在两个客户端当中,双方网络文件的上层都有自己的两种缓冲区,下层也是,所以不会冲突。(双方资源是隔离的) 也就是说我们上面用的read和write都是在对网络上下的两个缓冲区之间进行拷贝。

但TCP是面向字节流的,我们如何保证都上来的是一个完整的报文? 在用read和write的时候,TCP会有一个传输控制协议! 什么时候发,发多少,出错了如何解决? 也就是说write写的时候,其实从自己的缓冲区发送到网络的缓冲区就直接返回了,我们并不知道到底有没有发送到对方手里,因为这是TCP决定的。(其实就是给了OS,因为TCP也是在OS当中实现的,也就是TCP网络模块) 同理,read也是一样的。

所以这就需要协议定制,序列化和反序列化。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Socket 编程 UDP
  • Socket 编程 TCP
  • 守护进程
  • TCP简单的特性
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档