Linux环境应用编程(五):网络编程

 

一:引言

       Linux系统提供的经典进程间通讯机制(IPC):管道,共享内存,消息队列以及信号量,这些机制允许在同一台计算机上运行的进程可以互相通信,而对于不同计算机(通过网络相连)上的进程间通讯则引入了新的机制:网络进程间通信,进程可以通过套接字网络进程间通信接口互相通信,对于套接字接口可以采用许多不同的网络协议进行通信。

1、五层网络协议栈:

应用层协议:FTP、HTTP、SMTP

传输层协议:TCP协议、UDP协议

网络层协议:IP协议

本章讲述的TCP协议和UDP协议就属于传输层。关于五层结构具体功能参考:https://blog.csdn.net/weixin_37719279/article/details/82846226

2、TCP和UDP协议的区别

TCP向上层提供面向连接的可靠服务,UDP向上层提供无连接的不可靠服务,虽然UDP并没有TCP传输来的准确,但是在实时性要求高的场景下经常被用到。

3、TCP的三次握手和四次挥手

(1)三次握手

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

(2)四次挥手

a.客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
b.服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
c.客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
d.服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
e.客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
f.服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

详细参考:https://www.cnblogs.com/bj-mr-li/p/11106390.html

二:网络编程

1、寻址

(1)字节序

应用程序有时需要在处理器字节序与网络字节序之间转换。对于应用程序,有4个用来在处理器字节序和网络字节序之间转换的函数。

“h”表示“主机”字节序,“n”表示“网络”字节序,“l”表示“长”(即4字节)整数,“s”表示“短”(即2字节)整数

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostint32); 返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostint16); 返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netint32);  返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netint16);  返回值:以主机字节序表示的16位整数

(2)地址格式

        一个地址标识一个特定通信域的套接字端点,地址格式与这个特定的通信域相关,为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构:

struct sockaddr{
    unisgned short  sa_family; /*地址族*/
    char sa_data[14];          /*14字节的协议地址,包含该socket的IP地址和端口号。*/
};

由于系统兼容性问题,用新的地址结构表示:

struct sockaddr_in{
    unsigned short          sin_family;     /*地址族*/
    unsigned short int      sin_port;       /*端口号*/
    struct in_addr          sin_addr;       /*IP 地址*/
    unsigned char           sin_zero[8];    /*填充0 以保持与struct sockaddr 同样大小*/
}

(3)地址格式转化

通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制 IPv6 地址),而在通常使用的 socket 编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在 IPv4 中用到的函数有 inet_aton()、inet_addr()和 inet_ntoa(),而 IPv4 和 IPv6 兼容的函数有 inet_pton()和 inet_ntop()。

#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr)
int inet_ntop(int family, void *addrptr, char *strptr, size_t len)

family:{
    AF_INET:IPv4 协议
    AF_INET6:IPv6 协议
}
strptr:要转化的值
addrptr:转化后的地址
len:转化后值的大小
返回值:成功0、出错-1

inet_aton是一个改进的方法来将一个字符串IP地址转换为一个32位的网络序列IP地址。

1 输入参数string包含ASCII表示的IP地址。
2 输出参数addr是将要用新的IP地址更新的结构。

如果这个函数成功,函数的返回值非零。如果输入地址不正确则会返回零。使用这个函数并没有错误码存放在errno中,所以他的值会被忽略。

示例:

struct sockaddr_in socket_server_addr;

socket_server_addr.sin_family   = AF_INET;
/*主机字节序转换为网络字节序*/
socket_server_addr.sin_port     = htons(SERVER_PORT);
if (inet_aton(“192.168.1.100”, &socket_server_addr.sin_addr) == 0)
{
    printf("invalid server ip\n");
    return -1;
}

(4)名字地址转化

通常,人们在使用过程中都不愿意记忆冗长的 IP 地址,尤其到 IPv6 时,地址长度多达 128 位,那时就更加不可能一次次记忆那么长的 IP 地址了。因此,使用主机名将会是很好的选择。在 Linux 中,同样有一些函数可以实现主机名和地址的转化,最为常见的有 gethostbyname()、gethostbyaddr()和 getaddrinfo()等,它们都可以实现 IPv4 和 IPv6 的地址和主机名之间的转化。其中 gethostbyname()是将主机名转化为 IP 地址,gethostbyaddr()则是逆操作,是将 IP 地址转化为主机名,另外 getaddrinfo()还能实现自动识别 IPv4 地址和 IPv6地址。

struct hostent
{
    char *h_name;/*正式主机名*/
    char **h_aliases;/*主机别名*/
    int h_addrtype;/*地址类型*/
    int h_length;/*地址字节长度*/
    char **h_addr_list;/*指向 IPv4 或 IPv6 的地址指针数组*/
}

struct addrinfo
{
    int ai_flags;/*AI_PASSIVE, AI_CANONNAME;*/
    int ai_family;/*地址族*/
    int ai_socktype;/*socket 类型*/
    int ai_protocol;/*协议类型*/
    size_t ai_addrlen;/*地址字节长度*/
    char *ai_canonname;/*主机名*/
    struct sockaddr *ai_addr;/*socket 结构体*/
    struct addrinfo *ai_next;/*下一个指针链表*/
}

ai_flags{
    AI_PASSIVE:该套接口是用作被动地打开
    AI_CANONNAME:通知 getaddrinfo 函数返回主机的名字
}
ai_family{
    AF_INET:IPv4 协议
    AF_INET6:IPv6 协议
    AF_UNSPEC:IPv4 或 IPv6 均可
}
ai_socktype {
    SOCK_STREAM:字节流套接字 socket(TCP)
    SOCK_DGRAM:数据报套接字 socket(UDP)
}
ai_protocol{
    IPPROTO_IP:IP 协议
    IPPROTO_IPV4:IPv4 协议 4 IPv4
    IPPROTO_IPV6:IPv6 协议
    IPPROTO_UDP:UDP
    IPPROTO_TCP:TCP
}
#include <netdb.h>

struct hostent *gethostbyname(const char *hostname)
hostname:主机名
返回值:成功返回hostent类型指针、错误-1

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints,struct addrinfo **result)
node:网络地址或者网络主机名
service:服务名或十进制的端口号字符串
hints:服务线索
result:返回结果
返回值:成功0、错误-1

(1)通常服务器端在调用 getaddrinfo()之前,ai_flags 设置 AI_PASSIVE,用于 bind()函数(用于端口和地址的绑定,后面会讲到),主机名 nodename 通常会设置为 NULL。
(2)客户端调用 getaddrinfo()时,ai_flags 一般不设置 AI_PASSIVE,但是主机名nodename 和服务名 servname(端口)则应该不为空。
(3) 即使不设置 ai_flags 为 AI_PASSIVE,取出的地址也可以被绑定,很多程序中ai_flags 直接设置为 0,即 3 个标志位都不设置,这种情况下只要 hostname 和 servname设置的没有问题就可以正确绑定。

示例:

/* getaddrinfo.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    struct addrinfo hints, *res = NULL;
    int rc;
    memset(&hints, 0, sizeof(hints));

    /*设置 addrinfo 结构体中各参数 */
    hints.ai_flags = AI_CANONNAME;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_protocol = IPPROTO_UDP;
    /*调用 getaddinfo 函数*/
    rc = getaddrinfo("localhost", NULL, &hints, &res);
    if (rc != 0)
    {
        perror("getaddrinfo");
        exit(1);
    }
    else
    {
        printf("Host name is %s\n", res->ai_canonname);
    }
    exit(0);
}

2、socket基础编程

(1)套接字描述符

创建套接字:

#include <sys/socket.h>
int socket(int domain, int type,int protocol);

domain:确定通信的特性,包括地址格式
        AF_INET:IPv4因特网域
        AF_INET6:IPv6因特网域
        AF_UNIX:UNIX域
        AF_UPSPEC:未指定
type:确定套接字类型
        SOCK_DGRAM:固定长度的,无连接的,不可靠的报文传递(UDP)
        SOCK_RAW:IP协议的数据报接口
        SOCK_SEQPACKET:固定长度的,有序的,可靠的,面向连接的报文传递
        SOCK_STREAM:有序的,可靠的,双向的,面向连接的字节流(TCP)
protocol:通常为0,表示为给定的域和套接字类型选择默认协议

关闭套接字:

#include <sys/socket.h>
int shutdown(int sockfd, int how);

how:SHUT_RD(关闭读端),无法从套接字读取数据
     SHUT_WR(关闭写端),无法从套接字发送数据
     SHUT_RDWR,无法读取和发送数据

(2)服务器关联套接字与地址

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

示例:
struct sockaddr_in socket_server_addr;
duty_socket = socket(AF_INET, SOCK_STREAM, 0);
/* 服务器端填充 sockaddr_in结构 */
socket_server_addr.sin_family   = AF_INET;
/*端口号转换为网络字节序*/
socket_server_addr.sin_port     = htons(SERVER_PORT);
/*接收本机所有网口的数据*/
socket_server_addr.sin_addr.s_addr  = INADDR_ANY;
memset(socket_server_addr.sin_zero, 0, 8);

/* 捆绑sockfd描述符 */
ret = bind(duty_socket, (const struct sockaddr *)&socket_server_addr, sizeof(struct sockaddr));

(3)服务器调用listen函数表示接受连接请求

int listen(int sockfd,int backlog);

ret = listen(duty_socket, C_QUEUE);

sockfd是bind后的文件描述符。
backlog设置请求排队的最大长度。当有多个客户端程序和服务端相连时,使用这个表示可以介绍的排队长度。
listen函数将bind的文件描述符变为监听套接字,返回的情况和bind一样。

(4)服务器获得连接请求并建立连接

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


int customer_socket;
customer_socket = accept(duty_socket, (struct sockaddr *)&socket_client_addr, &addr_len);

sockfd是listen后的文件描述符。
addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了, bind,listen和accept是服务器端用的函数。
accept调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接。 accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了,失败时返回-1 。

(5)客户端建立连接

int connect(int sockfd, struct sockaddr * serv_addr,int addrlen);

ret = connect(client_socket, (const struct sockaddr *)&socket_server_addr, sizeof(struct sockaddr));


可以用connect建立一个连接,在connect中所指定的地址是想与之通信的服务器的地址。
sockfd是socket函数返回的文件描述符。
serv_addr储存了服务器端的连接信息,其中sin_add是服务端的地址。
addrlen是serv_addr的长度 
connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符,失败时返回-1。

(6)发送数据

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

sockfd 指定发送端套接字描述符;
buf 指明一个存放应用程序要发送数据的缓冲区;
len 指明实际要发送的数据的字节数;
flags 一般置0。
客户或者服务器应用程序都用send函数来向TCP连接的另一端发送数据


ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
sendto和send相似,区别在于sendto允许在无连接的套接字上指定一个目标地址。
dest_addr 表示目地机的IP地址和端口号信息,
addrlen 常常被赋值为sizeof (struct sockaddr)。
sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。

(7)接受数据

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

sockfd 指定接收端套接字描述符;
buf 指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
len 指明buf的长度;
flags 一般置0。
客户或者服务器应用程序都用recv函数从TCP连接的另一端接收数据。


ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);


recvfrom通常用于无连接套接字,因为此函数可以获得发送者的地址。
src_addr 是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。
addrlen 常置为sizeof (struct sockaddr)。

TCP示例:

该实例分为客户端和服务器端两部分,其中服务器端首先建立起 socket,然后与本地端口进行绑定,接着就开始接收从客户端的连接请求并建立与它的连接,接下来,接收客户端发送的消息。客户端则在建立socket 之后调用 connect()函数来建立连接。

服务端:

/*server.c*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define PORT 4321
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
int main()
{
    struct sockaddr_in server_sockaddr,client_sockaddr;
    int sin_size,recvbytes;
    int sockfd, client_fd;
    char buf[BUFFER_SIZE];
    /*建立 socket 连接*/
    if ((sockfd = socket(AF_INET,SOCK_STREAM,0))== -1)
    {
        perror("socket");
        exit(1);
    }
    printf("Socket id = %d\n",sockfd);
    /*设置 sockaddr_in 结构体中相关参数*/
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_sockaddr.sin_zero), 8);
    int i = 1;/* 允许重复使用本地地址与套接字进行绑定 */
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
    /*绑定函数 bind()*/
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr,
    sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }
    printf("Bind success!\n");
    /*调用 listen()函数,创建未处理请求的队列*/
    if (listen(sockfd, MAX_QUE_CONN_NM) == -1)
    {
        perror("listen");
        exit(1);
    }
    printf("Listening....\n");
    /*调用 accept()函数,等待客户端的连接*/
    if ((client_fd = accept(sockfd,(struct sockaddr *)&client_sockaddr, &sin_size)) == -1)
    {
        perror("accept");
        exit(1);
    }
    /*调用 recv()函数接收客户端的请求*/
    memset(buf , 0, sizeof(buf));
    if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) == -1)
    {
        perror("recv");
        exit(1);
    }
    printf("Received a message: %s\n", buf);
    close(sockfd);
    exit(0);
}

客户端:

/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 4321
#define BUFFER_SIZE 1024
int main(int argc, char *argv[])
{
    int sockfd,sendbytes;
    char buf[BUFFER_SIZE];
    struct hostent *host;
    struct sockaddr_in serv_addr;
    if(argc < 3)
    {
        fprintf(stderr,"USAGE: ./client Hostname(or ip address) Text\n");
        exit(1);
    }
    /*地址解析函数*/
    if ((host = gethostbyname(argv[1])) == NULL)
    {
        perror("gethostbyname");
        exit(1);
    }
    memset(buf, 0, sizeof(buf));
    sprintf(buf, "%s", argv[2]);
    /*创建 socket*/
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }
    /*设置 sockaddr_in 结构体中相关参数*/
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
    bzero(&(serv_addr.sin_zero), 8);
    /*调用 connect 函数主动发起对服务器端的连接*/
    if(connect(sockfd,(struct sockaddr *)&serv_addr,
    sizeof(struct sockaddr))== -1)
    {
        perror("connect");
        exit(1);
    }
    /*发送消息给服务器端*/
    if ((sendbytes = send(sockfd, buf, strlen(buf), 0)) == -1)
    {
        perror("send");
        exit(1);
    }
    close(sockfd);
    exit(0);
}

结果:

$ ./server
Socket id = 3
Bind success!
Listening....
Received a message: Hello,Server!
$ ./client localhost(或者输入 IP 地址) Hello,Server!

3、网络高级编程

在实际情况中,人们往往遇到多个客户端连接服务器端的情况。由于之前介绍的如 connet()、recv()和 send()等都是阻塞性函数,如果资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理 I/O 多路复用的情况了。本节给出了两种解决 I/O 多路复用的解决方法,这两个函数都是之前学过的 fcntl()和select()。

(1)fcntl()

函数 fcntl()针对 socket 编程提供了如下的编程特性。

  • 非阻塞 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_NONBLOCK。
  • 异步 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_ASYNC。
/* net_fcntl.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#define PORT 1234
#define MAX_QUE_CONN_NM 5
#define BUFFER_SIZE 1024
int main()
{
    struct sockaddr_in server_sockaddr, client_sockaddr;
    int sin_size, recvbytes, flags;
    int sockfd, client_fd;
    char buf[BUFFER_SIZE];
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_sockaddr.sin_zero), 8);
    int i = 1;/* 允许重复使用本地地址与套接字进行绑定 */
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }
    if(listen(sockfd,MAX_QUE_CONN_NM) == -1)
    {
        perror("listen");
        exit(1);
    }
    printf("Listening....\n");
    /* 调用 fcntl()函数给套接字设置非阻塞属性 */
    flags = fcntl(sockfd, F_GETFL);
    if (flags < 0 || fcntl(sockfd, F_SETFL, flags|O_NONBLOCK) < 0)
    {
        perror("fcntl");
        exit(1);
    }
    while(1)
    {
        sin_size = sizeof(struct sockaddr_in);
        if ((client_fd = accept(sockfd,(struct sockaddr*)&client_sockaddr, &sin_size)) < 0)
        {
            perror("accept");
            exit(1);
        }
        if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) < 0)
        {
            perror("recv");
            exit(1);
        }
        printf("Received a message: %s\n", buf);
    } /*while*/
    close(client_fd);
    exit(1);
}



$ ./net_fcntl
Listening....
accept: Resource temporarily unavailable
可以看到,当 accept()的资源不可用(没有任何未处理的等待连接的请求)时,程序就会自动返回。

(2)select()

使用 fcntl()函数虽然可以实现非阻塞 I/O 或信号驱动 I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的 CPU 资源的占用。在这里可以使用 select()函数来解决这个问题,同时,使用 select()函数还可以设置等待的时间,可以说功能更加强大。

/* net_select.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#define PORT 4321
#define MAX_QUE_CONN_NM 5
#define MAX_SOCK_FD FD_SETSIZE
#define BUFFER_SIZE 1024
int main()
{
    struct sockaddr_in server_sockaddr, client_sockaddr;
    int sin_size, count;
    fd_set inset, tmp_inset;
    int sockfd, client_fd, fd;
    char buf[BUFFER_SIZE];
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_sockaddr.sin_zero), 8);
    int i = 1;/* 允许重复使用本地地址与套接字进行绑定 */
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }
    if(listen(sockfd, MAX_QUE_CONN_NM) == -1)
    {
        perror("listen");
        exit(1);
    }
    printf("listening....\n");
    /*将调用 socket()函数的描述符作为文件描述符*/
    FD_ZERO(&inset);
    FD_SET(sockfd, &inset);
    while(1)
    {
        tmp_inset = inset;
        sin_size=sizeof(struct sockaddr_in);
        memset(buf, 0, sizeof(buf));
        /*调用 select()函数*/
        if (!(select(MAX_SOCK_FD, &tmp_inset, NULL, NULL, NULL) > 0))
        {
            perror("select");
        }
        for (fd = 0; fd < MAX_SOCK_FD; fd++)
        {
            if (FD_ISSET(fd, &tmp_inset) > 0)
            {
                if (fd == sockfd)
                { /* 服务端接收客户端的连接请求 */
                    if ((client_fd = accept(sockfd,(struct sockaddr *)&client_sockaddr, &sin_size))== -1)
                    {
                        perror("accept");
                        exit(1);
                    }
                    FD_SET(client_fd, &inset);
                    printf("New connection from %d(socket)\n", client_fd);
                }
                else /* 处理从客户端发来的消息 */
                {
                    if ((count = recv(client_fd, buf, BUFFER_SIZE, 0)) > 0)
                    {
                        printf("Received a message from %d: %s\n",
                        client_fd, buf);
                    }
                    else
                    {
                        close(fd);
                        FD_CLR(fd, &inset);
                        printf("Client %d(socket) has left\n", fd);
                    }
                }
            } /* end of if FD_ISSET*/
        } /* end of for fd*/
    } /* end if while while*/
    close(sockfd);
    exit(0);
}


$ ./server
listening....
New connection from 4(socket) /* 接受第一个客户端的连接请求*/
Received a message from 4: Hello,First! /* 接收第一个客户端发送的数据*/
New connection from 5(socket) /* 接受第二个客户端的连接请求*/
Received a message from 5: Hello,Second! /* 接收第二个客户端发送的数据*/
Client 4(socket) has left /* 检测到第一个客户端离线了*/
Client 5(socket) has left /* 检测到第二个客户端离线了*/
$ ./client localhost Hello,First! & ./client localhost Hello,Second

猜你喜欢

转载自blog.csdn.net/qq_34968572/article/details/107513609