一:引言
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