简单实现的socket

简单实现的socket

1.1 socket 简介

        Socket(套接字)是在网络编程中用于实现不同计算机之间通信的接口和抽象。它允许应用程序通过网络发送和接收数据,实现网络通信。

        前面我们了解到了编写一个网络程序如果遵循 TCP/IP 四层模型的话我们应该对每一层都要进行处理,网络层使用 IP 协议、传输层使用 TCP 或 UDP 协议等等,这些真的需要我们自己进行处理嘛,当然不是,这些底层的协议处理已经集成在你的系统中了,我们只需要通过一个接口去使用即可,至于底层的代码究竟是如何实现的完全不用你操心,也就是说我们不会直接接触 TCP、UDP、IP 等底层协议。

        套接字类型: Socket有不同的类型,常见的有流式套接字(SOCK_STREAM)数据报套接字(SOCK_DGRAM)。流式套接字提供了可靠的、面向连接的通信,如 TCP 协议;数据报套接字提供了不可靠的、无连接的通信,如 UDP 协议。

        网络通信的流程需要 这四个函数 socket(创建)bind(绑定)listen(监听)accept(接收)

        创建套接字: 在编程中,首先需要创建一个套接字。调用socket()函数,指定套接字的域(如AF_INET表示 IPv4)、类型和协议,返回一个套接字描述符,linux 中通过一个系统调用 socket 来创建一个套接字,下面是他的函数原型:

1.2 select()函数--创建套接字 

1.2.1 函数描述

        创建一个套接字

1.2.2 头文件

        #include <sys/socket.h>

1.2.3 函数原型

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

1.2.4 函数参数

        1、参数 domain

        domain是“域”的意思,其值为AF_INET

        在Linux系统中,domain参数用于指定套接字的协议域(protocol domain),它定义了套接字通信的协议族。以下是Linux系统中一些常见的domain值:

        AF_UNIX:Unix 域协议域用于本地通信(Inter-process communication,IPC)。它使用文件路径作为套接字地址,用于同一台机器上的进程间通信。

        AF_INET:IPv4 协议域,用于 Internet 地址族。这是最常见的协议域,用于基于 IPv4 的网络通信

        AF_INET6:IPv6 协议域,用于 IPv6 地址族。这是用于基于 IPv6 的网络通信。

        AF_PACKET:用于原始网络数据包的协议域。它允许应用程序直接访问网络帧,适用于网络协议分析和数据包捕获等场景。

        AF_BLUETOOTH:蓝牙协议域,用于蓝牙通信。

        AF_X25:X.25 协议域,用于 X.25 网络协议

        AF_NETLINK:Netlink 协议域,用于 Linux 内核与用户空间进程之间的通信

        AF_PACKET:原始数据链路层套接字,允许应用程序直接访问数据链路层帧。

        2、参数 type

        type指定套接字的类型,可以是以下值之一:

        SOCK_STREAM流套接字,用于可靠、面向连接的服务。对应于 TCP 协议

        SOCK_DGRAM数据报套接字,用于无连接、不可靠的服务。对应于 UDP 协议。

        SOCK_SEQPACKET:顺序数据包套接字,在 SCTP 协议中使用。

        SOCK_RAW原始套接字,用于直接访问底层网络协议。可以自定义协议头部并发送。

        SOCK_RDM:可靠数据报套接字,很少使用。

        SOCK_PACKET:废弃的套接字类型,已经不再使用。

        3、参数protocol

        在socket函数中,protocol参数用于指定套接字使用的协议。协议(protocol)是一组规则和约定,用于在网络中的不同节点之间进行通信和数据交换。下面是一些常见的protocol参数值及其对应的协议:

        IPPROTO_TCP:TCP(Transmission Control Protocol)协议。它是一种面向连接的、可靠的、基于字节流的传输协 议,用于提供可靠的数据传输。

        IPPROTO_UDP:UDP(User Datagram Protocol)协议。它是一种无连接的、不可靠的、基于数据报的传输协议,用 于提供快速的数据传输,但不保证数据的可靠性和顺序性。

        IPPROTO_SCTP:SCTP(Stream Control Transmission Protocol)协议。它是一种面向连接的、可靠的、基于消息 的传输协议,提供了可靠的数据传输和流量控制等功能。

        IPPROTO_ICMP:ICMP(Internet Control Message Protocol)协议。它是一种网络层协议,用于在网络中传递控制 信息和错误报文,如网络不可达、请求超时等。

        IPPROTO_IGMP:IGMP(Internet Group Management Protocol)协议。它是一种组播协议,用于在 IP 网络中进行组播组的管理和维护。

IPPROTO_RAW:原始 IP 协议。它允许应用程序直接访问网络层的数据,可用于构造和发送自定义的 IP 报文。

        需要注意的是,protocol参数的具体取值取决于所选择的协议域(domain)和套接字类型(type)。在某些情况下,可以将protocol设置为0表示使用默认协议。此时,系统会根据协议域和套接字类型自动选择适合的协议。

        参数type和参数protocol之间的关系

        一般来说:

        SOCK_STREAM 对应 IPPROTO_TCP

        SOCK_DGRAM 对应 IPPROTO_UDP

        SOCK_SEQPACKET 对应IPPROTO_SCTP

        SOCK_RAW 对应IPPROTO_ICMP、IPPROTO_RAW和IPPROTO_IGMP

        由此,你可以大概知道当Linux中的socket函数的参数domain和参数type确定后,参数protocol该怎么选。

1.2.5 函数返回值

        如果没有发生错误,socket将返回一个引用新socket的描述符。

        否则,将返回INVALID_SOCKET的值,并且可以通过调用WSAGetLastError检系特定的错误代码。

1.2.6 创建套接字代码

    int lfd = socket(AF_INET,SOCK_STREAM,0);  // 创建一个流式套接字
    if(lfd<0) // 判断创建是否成功
    {
        perror("socket error");
        exit(1);
    }
    int lfd1 = socket(AF_INET,SOCK_DGRAM,0);  // 创建一个报式套接字
    int lfd2 = socket(AF_INET,SOCK_RAW,0);  // 创建一个原始套接字

1.3 bind()函数--绑定IP+端口号

        创建套接字之后,还得将套接字和 IP地址与端口号绑定

        套接字绑定 IP 地址和端口号:服务器端需要将套接字与特定的IP地址和端口号绑定起来,使用bind()函数。

1.3.1 函数描述

        将套接字与特定的IP地址和端口号绑定

1.3.2 函数原型

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

1.3.3 函数参数

        1、int sockfd:要绑定的socket描述符

        2、const struct sockaddr *addr:一个结构体地址,用来做缓冲区(这个结构体已经弃用,用的是下面的这个sockaddr_in)结构体所需的头文件为:#include <netinet/ip.h>

#include <netinet/ip.h>
struct sockaddr_in
{
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */(网络字节序的端口号)
    struct in_addr sin_addr;   /* internet address */存储IP地址
};

        (1)sin_family指代协议族,在socket编程中只能是AF_INET

        (2)sin_port存储端口号(使用网络字节顺序),2个字节(16位,0-65535),端口号一般大于1000,找个没人用的端口号,这里需要考虑大小端的问题,也就是网络字节序和主机的大小端是否相同(一般不同)一般主机的字节序是小端存储、网络字节序是大端存储因此需要将端口号的存储方式改为网络字节序。修改大小端,使用 htons() 函数。

// 自定义端口号
// 需要考虑大小端的问题,也就是网络字节序和主机的大小端是否相同(一般不同)
// 一般主机的字节序是小端存储、网络字节序是大端存储
// 因此,需要自己转换字节序
#define SOCKPORT 8001

        (3)sin_addr存储IP地址,使用in_addr这个结构体,该结构体如下

struct in_addr
{
    uint32_t  s_addr;   /* address in network byte order */(网络字节序的IP地址)
};

        在终端使用ifconfig ,查看IP地址为 192.168.x.xxx(每个人的都不一样) ,这是一个主机(本地)字节序的字符串,而s_addr的类型是uint32_t,应该是一个32位(4个字节的)整型因此要将 上面 本机字节序的字符串 转换为 一个 网络字节序的整型使用 inet_pton()函数转换 或者 直接传值 INADDR_ANY 这是一个宏定义,值为0,传入之后可以是本机的任意一个IP地址。

        3、socklen_t addrlen:结构体缓冲区的长度

1.3.4 函数返回值

        成功返回0

        失败返回-1,并记录错误信息

1.3.5 大小端问题     

        1、大端存储与小端存储

        大端存储、小端存储:数据的存储方式(目前一般是小端)

        小端:高字节(高位)存在高地址,低字节(低位)存在低地址

        小端:高字节(高位)存在低地址,低字节(低位)存在高地址

         2、网络字节序和主机字节序的转换 htons

// 自定义端口号
#define SOCKPORT 8001

        sin_port存储端口号(使用网络字节顺序),2个字节(short类型)(16位,0-65535),端口号一般大于1000,找个没人用的端口号,这里需要考虑大小端的问题,也就是网络字节序和主机的大小端是否相同(一般不同),一般主机的字节序是小端存储、网络字节序是大端存储,因此需要将端口号的存储方式改为网络字节序。修改大小端,使用 htons() 函数。

        3、hton函数族,使用 htons() 函数将短整型的端口号从主机字节序转为网络字节序

      直接说htons无法理解,该函数全称为 host to network short,这样是否能够理解了?也就是将short类型的数据从本机字节序(小端存储) 转换为 网络字节序(大端存储)。还函数族还有以下几个函数。

        uint32_t htonl(uint32_t hostlong);

        uint16_t htons(uint16_t hostshort);

       uint32_t ntohl(uint32_t netlong);

       uint16_t ntohs(uint16_t netshort)

        4、使用联合体检测大小端   

// 联合体检测大小端
union Un
{
    int a;
    char c;
}un;
int main()
{
    un.a = 1; 
    if(un.c)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }

    return 0;
}

1.3.6  绑定IP+端口代码

    // 使用 bind()函数,绑定IP地址、端口号
    struct sockaddr_in serAddr; // 创建一个结构体,用来传参
    /*
    sockaddr_in 结构体的参数
    in_family指代协议族,在socket编程中只能是AF_INET
	sin_port存储端口号(使用网络字节顺序),2个字节(16位,0-65535)
    sin_addr存储IP地址,使用in_addr这个数据结构
    */
    serAddr.sin_family  = AF_INET;
    // 端口号一般大于1000,找个没人用的端口号
    // 一般主机的字节序是小端存储、网络字节序是大端存储
    // 因此,需要自己转换字节序
    serAddr.sin_port = htons(SOCKPORT);
    // htons 将主机字节序转为网络字节序(短整型)
    // host to network short
    // 主机 向  网络    短整型

    // ip 地址 192.168.10.128 --- 主机(本地)字节序的字符串
    // uint32_t 应该是一个32位(4个字节的)整型
    // 因此要将 上面 本机字节序的字符串 转换为 一个 网络字节序的整型
    serAddr.sin_addr.s_addr = INADDR_ANY;
    // 使用 inet_pton()函数转换
	// 或者直接传入 INADDR_ANY 这是一个宏定义,值为0,传入之后可以是本机的任意一个IP地址

    // 给套接字绑定 IP地址、端口号
    int bret = bind(lfd,(struct sockaddr *)&serAddr,sizeof(serAddr));
    if(bret<0) // 判断绑定是否成功
    {
        perror("bind error");
        exit(1);
    }

1.4 listen()函数

        用来监听与服务连接的请求,到这里,如果有客户端与服务器进行连接,已经完成了三次握手请求。

	// 得有一个接收方
	// 监听,等着 lfd发送内容,第二个参数传一个大于0的数
    listen(lfd,64);

        没有accept也能完成三次握手请求

        我们先看一下,三次握手的过程

          1、服务端进程准备好接收来自外部的TCP连接,一般情况下是调用socket、bind、listen三个函数完成。这种打开方式被认为是被动打开(passiveopen)。然后服务端进程处于LISTEN状态,等待客户端连接请求。

         2、客户端通过connect发起主动打开(active open),向服务器发出连接请求,请求中首部同步位SYN=1,同时选择一个初始序号sequence,简写seg=x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入SYN-SEND 状态。

        3、服务器收到客户端连接后,需要确认客户端的报文段。在确认报文段中,把SYN和ACK位都置为1。确认号是ack=x+1,同时也为自己选择一个初始序号seq=y。这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP服务器进入SYN-RECEIVED(同步收到)状态。

        4、客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的ACK置为1,序号为seq=x+1,确认号为ack=y+1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是seq=x+1。这时,客户端进入ESTABLISHED(已连接)状态。

        5、服务器收到客户的确认后,也进入ESTABLISHED状态。

         这是一个典型的三次握手过程,通过上面3个报文段就能够完成一个TCP连接的建立。三次握手的的目的不仅仅在于让通信双方知晓正在建立一个连接,也在于利用数据包中的选项字段来交换一些特殊信息,交换初始序列号。一般首个发送SYN 报文的一方被认为是主动打开一个连接,而这一方通常也被称为客户端。而SYN的接收方通常被称为服务端,它用于接收这个SYN,并发送下面的SYN,因此这种打开方式是被动打开。

 下面是没有accept函数的代码

		    // 没有accpet,也能完成三次握手
int main(int argc, char* argv[])
{
	// 创建socket,ip协议,套接字类型(流式套接字),流式套接字默认TCP协议
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd<0)
    {
        perror("socket error");
    }
    // 绑定ip 端口号
    struct sockaddr_in serAddr,cliAddr;
    // 协议族,在socket编程中只能是AF_INET
    serAddr.sin_family = AF_INET;
    // 端口号
    serAddr.sin_port = htons(SOCKPORT);
    // ip地址,本机任意一个ip
    serAddr.sin_addr.s_addr = INADDR_ANY;
    int bret = bind(lfd,(struct sockaddr*)&serAddr,sizeof(serAddr));
    if(bret < 0)
    {
        perror("bind error");
    }
    // 监听
    listen(lfd,64);
    while(1);
    return 0;
}

        编译后运行这段代码,使服务器处于listen状态

        使用 netstat -apn | grep 8001。可以看到,目前是LISTEN状态

         在终端使用 nc ip地址  端口号 进行连接后

        

        可以看到,连接后,已经变为ESTABLISHED状态,三次握手已经完成

        可以发现,有3个套接字,两个服务器的ldf。而有一个是 - ,这个套接字也是存在的,在内核中,我们是拿不到的,因为我们没有文件描述符接收他,因此accept 的作用就是将内核中这个套接字的文件描述符拿出来,拿到这个文件描述符后,才能和客户端通信。

        服务器端,可能会有很多客户端请求建立连接,此时就会有一个问题

        服务器如何知道,它与某个客户端已经完成了前两次握手,还没完成第三次握手呢?

        在被动套接字中,会有两个队列(半连接队列、全连接队列)

        在完成第二次握手后,就会将客户端套接字放入半连接队列

        在完成第三次握手后,就会将客户端套接字由半连接队列放入全连接队列

        而accept就会读取全连接队列中的套接字进行返回,如果全连接队列为空,accept就会阻塞

1.5 accep()函数

        

1.5.1 函数描述

        该函数是用于在服务器端接受客户端连接的系统调用

1.5.2 函数原型

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

1.5.3 函数参数

        1、int sockfd:是一个已经通过 socket 函数创建并绑定到特定地址的监听套接字(通常是服务器的监听套接字)

        2、struct sockaddr *addr:是一个指向 struct sockaddr 类型的指针,用于存储连接的对端地址信息。可以为 NULL,表示不关心对端地址。

        3、socklen_t *addrlen:是一个指向 socklen_t 类型的指针,用于指定 addr 缓冲区的大小。在调用 accept 之前,addrlen 应该被初始化为 struct sockaddr 缓冲区的大小,函数返回时,addrlen 会被设置为实际地址结构的长度。

1.5.3 函数返回值

        如果连接成功,返回一个新的文件描述符,这个文件描述符用于与客户端通信。

        如果失败,返回 -1,并设置 errno 表示错误原因。

struct sockaddr_in cliAddr;
socklen_t addrlen = sizeof(cliAddr);
int cfd = accept(lfd,(struct sockaddr*)&cliAddr,&addrlen);
if(cfd < 0)
{   // 判断是否连接成功
    perror("accept error");
    exit(1);
}
printf("accept return\n");
// 读取客户端的端口号
int port = ntohs(cliAddr.sin_port);
// 读取客户端的IP地址
char dst[64];
// 将网络中的二进制IP地址转为点分十进制字符串
inet_ntop(AF_INET,&cliAddr.sin_addr.s_addr,dst,sizeof(dst));
printf("client ip: %s\n",dst);
printf("client port: %d\n",port);

1.6 服务器与客户端的实现

        至此,我们就完成了,网络通信的流程需要的四个函数 socket(创建)、bind(绑定)、listen(监听)、accept(接收)

        客户端在终端向服务器发消息,服务器再将消息返回给客户端

   服务器代码

// 端口号
#define SOCKPORT 8002
// socket简单实现--服务器 server
int main(int argc, char* argv[])
{
    // 创建socket,ip协议,套接字类型(流式套接字),流式套接字默认TCP协议
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd<0)
    {
        perror("socket error");
        exit(1);
    }
    // 绑定ip 端口号
    struct sockaddr_in serAddr,cliAddr;
    // 协议族,在socket编程中只能是AF_INET
    serAddr.sin_family = AF_INET;
    // 端口号
    serAddr.sin_port = htons(SOCKPORT);
    // ip地址,本机任意一个ip
    serAddr.sin_addr.s_addr = INADDR_ANY;
    int bret = bind(lfd,(struct sockaddr*)&serAddr,sizeof(serAddr));
    if(bret < 0)
    {
        perror("bind error");
        exit(1);
    }
    // 监听
    listen(lfd,64);
    printf("listen return\n");
    // 接收
    socklen_t len = sizeof(cliAddr);
    int cfd = accept(lfd,(struct sockaddr*)&cliAddr,&len);
    if(cfd < 0)
    {
        perror("accept error");
        exit(1);
    }
    printf("accept return\n");
    char buf[1024];
    char buf1[1024];
    while(1)
    {
        // 读取客户端数据
        int read_count = read(cfd,buf,sizeof(buf));
        // 写到终端
        write(STDOUT_FILENO,buf,read_count);
        // 发回客户端
        write(cfd,buf1,read_count1);
    }
    return 0;
}

 客户端代码

// IP 端口号
#define SERIP "192.168.xxx.xxx" // 输入自己的ip地址
#define SERPORT 8001
// // socket简单实现 --- 客户端 client
int main(int argc, char* argv[])
{
    // 创建socket,ip协议,套接字类型(流式套接字),流式套接字默认TCP协议
    int cfd = socket(AF_INET,SOCK_STREAM,0);
    if(cfd<0)
    {
        perror("socket error");
        exit(1);
    }
    // 客户端是不需要绑定的,他只需要知道给谁打电话就行
    // 绑定ip 端口号
    struct sockaddr_in serAddr;
    // 协议族,在socket编程中只能是AF_INET
    serAddr.sin_family = AF_INET;
    // 存要连接服务器的端口号
    serAddr.sin_port = htons(SERPORT);
    // 存要连接的服务器的ip地址
    // int inet_pton(int af, const char *src, void *dst);
    // 将 "192.168.10.128" 点分十进制的字符串 转为正确格式
    
    inet_pton(AF_INET,SERIP,&serAddr.sin_addr.s_addr);
   
    // 客户端也不需要 accept,但是需要另一个函数 connect
    // 用来申请与服务器进行连接--进行三次握手
    connect(cfd,(struct sockaddr*)&serAddr,sizeof(serAddr));
    
    printf("connect successful\n");
    
    // 发送消息
    char buf[1024];
    char buf1[1024];
    while(1)
    {
        // 读取终端的数据到buf  
        int read_count = read(STDIN_FILENO,buf,sizeof(buf));
        // 发给到服务器
        write(cfd,buf,read_count);
    }
    return 0;
}

        这样,我们就简单的实现了一个利用socket实现网络通信的流程。

socket客户端服务器总结

服务器端:

1、socket(tcp:lfd)

lfd的目的是和客户端建立三次握手,是一个被动套接字(等待别人给他发送握手请求,不参与通信)因为是被动套接字,因此它需要能被找到,就需要显示的绑定IP和端口号(目的是用户知道它使用的IP和端口,让用户通过这个IP和端口,与他进行连接)。

2、bind():显示的绑定IP和端口号

3、listen():将主动套接字变为被动套接字

LISTEN状态,等待别人申请建立连接(进行三次握手)

4、在listen之后,lfd就可以与客户端建立连接了。

5、被动套接字:两个队列(用来记录握手进度)

半连接队列:三次握手不是瞬间完成的,有一个过程,因此需要一个容器来记录完成第二次握手的对象(半连接对象),这个半连接对象主要的数据就是描述一对连接(源IP:源端口号-目的IP:目的端口号)全连接队列:用来标记所有已经完成三次握手的连接,还没有交给应用层处理。

6、accept():从全连接队列中,取出一个已完成三次握手的连接,返回一个socket文件描述符(这个文件描述符指向是和客户端cfd通信的socket描述符),这时,服务器多了一个socket套接字cfd,利用这个cfd就可以与客户端进行通信了。(利用lfd读写是没用的,lfd的作用是转接)。因此每多一个客户端的连接,服务器就会多一个套接字。

        所以用于通信的socket总是成对存在的

客户端:

1、socket(tcp:cfd):向服务器发起三次握手请求,与服务器建立三次握手,用于和服务器通信的套接字,是一个主动套接字。 因为是主动套接字,它只需要知道别人的IP和端口就行,也不需要别人申请与他连接,因此也没有必要显示自己的IP和端口(系统会帮它绑定IP和端口)。(就像打电话,你只需要知道别人的电话号即可,自己有电话就行

2、connect():主动向服务器发起建立连接的行为。有可能会阻塞,在建立连接之前,该函数不会返回,会阻塞(在三次握手期间,connect会阻塞,只不过时间短)

3、此时就可以利用cfd描述符与服务器进行通信了。

1.7 多进程服务器

        上面,我们简单的实现了一个利用socket实现网络通信的流程。

        但是,上面的实现代码是存在问题的,是无法完成多个客户端与服务器进行通信的。

        因为,一旦第一个客户端与服务器进行连接,服务器就会进入while循环

        当第二个客户端申请连接时,就无法进行accept了。

         问题分析:

                我们既需要关注accept(是否有客户端与服务器申请连接),又需要关注read(是否有客户端向服务器发送消息),而进程只有一个,无法同时关注这两个事件。(就像狡兔三窟,一个人,是无法堵住三个洞的,得找三个人,才行

        因此,我们需要创建多个进程来关注不同的事件

        我们让父进程关注accept,子进程进行读写操作,每有一个客户端申请与服务器进行连接,就创建一个子进程来让他进行读取操作。

// 端口号
#define SOCKPORT 8001
// socket简单实现--多进程服务器 server
int main(int argc, char* argv[])
{
    // 创建socket,ip协议,套接字类型(流式套接字),流式套接字默认TCP协议
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd<0)
    {
        perror("socket error");
        exit(1);
    }
    // 绑定ip 端口号
    struct sockaddr_in serAddr,cliAddr;
    // 协议族,在socket编程中只能是AF_INET
    serAddr.sin_family = AF_INET;
    // 端口号
    serAddr.sin_port = htons(SOCKPORT);
    // ip地址,本机任意一个ip
    serAddr.sin_addr.s_addr = INADDR_ANY;
    int bret = bind(lfd,(struct sockaddr*)&serAddr,sizeof(serAddr));
    if(bret < 0)
    {
        perror("bind error");
        exit(1);
    }
    // 监听
    listen(lfd,64);
    printf("listen return\n");
    // 接收
    socklen_t len = sizeof(cliAddr);
    while(1)
    {
        // 父进程进行连接
        int cfd = accept(lfd,(struct sockaddr*)&cliAddr,&len);
        if(cfd < 0)
        {
            perror("accept error");
            exit(1);
        }
        printf("accept return\n");
        // 读取客户端的端口号
        int port = ntohs(cliAddr.sin_port);
        // 读取客户端的IP地址
        char dst[64];
        // 将网络中的二进制IP地址转为点分十进制字符串
        inet_ntop(AF_INET,&cliAddr.sin_addr.s_addr,dst,sizeof(dst));
        printf("client ip: %s\n",dst);
        printf("client port: %d\n",port);
        
        int pid = fork();
        if(pid == 0) // 子进程发送消息
        {
            close(lfd); // 子进程,将无用的lfd关闭
            while(1)
            {   char buf[1024];
                // 读取客户端数据
                int read_count = read(cfd,buf,sizeof(buf));
                // 写到终端
                write(STDOUT_FILENO,buf,read_count);
            
                // 发回客户端
                write(cfd,buf,read_count);
            }
        }
        close(cfd); // 父进程用不到cfd,关闭
    }
    return 0;
}

        每当有一个新的客户端建立连接,就会创建一个新的进程为这个客户端服务当某一个客户端断开连接时,子进程终止、

        问题分析:

        ·频繁的创建进程和销毁进程,系统开销较大

        ·能够承载的上限低

        ·能否实现一个进程就可以和多个客户端进行通信?(多路IO

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值