目录

C++

socket编程演示

socket() 创建套接字

通过 socket() 函数创建了一个套接字,参数 AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用面向连接的数据传输方式,IPPROTO_TCP 表示使用 TCP 协议。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int socket(int af, int type, int protocol);//linux
SOCKET socket(int af, int type, int protocol);//windows

//linux
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

//windows
SOCKET tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
SOCKET udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

参数:

  • af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
  • type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM,在《socket是什么意思》一节中已经进行了介绍。
  • protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议.

返回值:

  • 返回值为 SOCKET 类型,也就是句柄。

bind() 绑定

通过 bind() 函数将套接字 serv_sock 与特定的IP地址和端口绑定,IP地址和端口都保存在 sockaddr_in 结构体中。 bind() 函数的原型为:

1
2
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

这里我们使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型 接下来不妨先看一下 sockaddr_in 结构体,它的成员变量如下:

1
2
3
4
5
6
struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};
1
2
3
struct in_addr{
    in_addr_t  s_addr;  //32位的IP地址
};

sockaddr_in6,用来保存 IPv6 地址

1
2
3
4
5
6
7
struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6
    in_port_t sin6_port;  //(2)16位端口号
    uint32_t sin6_flowinfo;  //(4)IPv6流信息
    struct in6_addr sin6_addr;  //(4)具体的IPv6地址
    uint32_t sin6_scope_id;  //(4)接口范围ID
};

listen()监听

1
2
int listen(int sock, int backlog);  //Linux
int listen(SOCKET sock, int backlog);  //Windows
  • sock 为需要进入监听状态的套接字
  • backlog 为请求队列的最大长度。并发量小的话可以是10或者20。如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

accept() 接收请求

accept() 函数用来接收客户端的请求。程序一旦执行到 accept() 就会被阻塞(暂停运行),直到客户端发起请求。 当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

1
2
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);  //Windows

它的参数与 listen() 和 connect() 是相同的:

  • sock 为服务器端套接字
  • addr 为 sockaddr_in 结构体变量
  • addrlen 为参数 addr 的长度,可由 sizeof() 求得。
  • accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

send()发送数据

1
int send(SOCKET sock, const char *buf, int len, int flags);
  • sock 为要发送数据的套接字
  • buf 为要发送的数据的缓冲区地址
  • len 为要发送的数据的字节数
  • flags 为发送数据时的选项。

flags 参数一般设置为 0 或 NULL,初学者不必深究。

connect()发起请求

connect() 向服务器发起请求,服务器的IP地址和端口号保存在 sockaddr_in 结构体中。直到服务器传回数据后,connect() 才运行结束。 connect() 函数用来建立连接,它的原型为:

1
2
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows

recv()接收数据

1
int recv(SOCKET sock, char *buf, int len, int flags);
  • sock 为要接收数据的套接字
  • buf 为要接收的数据的缓冲区地址
  • len 为要接收的数据的字节数
  • flags 为接收数据时的选项。

flags 参数一般设置为 0 或 NULL,初学者不必深究。

closesocket() 关闭套接字

close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。

shutdown()断开连接

1
2
int shutdown(int sock, int howto);  //Linux
int shutdown(SOCKET s, int howto);  //Windows

sock 为需要断开的套接字,howto 为断开方式。 howto 在 Windows 下有以下取值:

  • SD_RECEIVE:关闭接收操作,也就是断开输入流。
  • SD_SEND:关闭发送操作,也就是断开输出流。
  • SD_BOTH:同时关闭接收和发送操作。

shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。

close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

WSACleanup()终止 DLL 的使用

DLL的加载

WinSock(Windows Socket)编程依赖于系统提供的动态链接库(DLL),有两个版本:

  • 较早的DLL是 wsock32.dll,大小为 28KB,对应的头文件为 winsock1.h;
  • 最新的DLL是 ws2_32.dll,大小为 69KB,对应的头文件为 winsock2.h。

使用DLL之前必须把DLL加载到当前程序,你可以在编译时加载,也可以在程序运行时加载,《C语言高级教程》中讲到了这两种加载方式,请猛击:动态链接库DLL的加载:隐式加载(载入时加载)和显式加载(运行时加载)。

这里使用#pragma命令,在编译时加载: #pragma comment (lib, "ws2_32.lib")

WSAStartup() 进行初始化

使用DLL之前,还需要调用 WSAStartup() 函数进行初始化,以指明 WinSock 规范的版本,它的原型为:

1
2
3
4
int WSAStartup(
	WORD wVersionRequested,
	LPWSADATA lpWSAData
);

ws2_32.dll 支持的最高版本为 2.2,建议使用的版本也是 2.2。

文件传输

实例:client 从 server 下载一个文件并保存到本地。 编写这个程序需要注意两个问题:

  1. 文件大小不确定,有可能比缓冲区大很多,调用一次 write()/send() 函数不能完成文件内容的发送。接收数据时也会遇到同样的情况。 要解决这个问题,可以使用 while 循环,例如:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//Server 代码
int nCount;
while( (nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0 ){
    send(sock, buffer, nCount, 0);
}
//Client 代码
int nCount;
while( (nCount = recv(clntSock, buffer, BUF_SIZE, 0)) > 0 ){
    fwrite(buffer, nCount, 1, fp);
}

对于 Server 端的代码,当读取到文件末尾,fread() 会返回 0,结束循环。

对于 Client 端代码,有一个关键的问题,就是文件传输完毕后让 recv() 返回 0,结束 while 循环。

  1. Client 端如何判断文件接收完毕,也就是上面提到的问题——何时结束 while 循环。

最简单的结束 while 循环的方法当然是文件接收完毕后让 recv() 函数返回 0,那么,如何让 recv() 返回 0 呢?recv() 返回 0 的唯一时机就是收到FIN包时。

本节以Windows为例演示文件传输功能,Linux与此类似,不再赘述。请看下面完整的代码。

服务器端 server.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 1024
int main(){
    //先检查文件是否存在
    char *filename = "D:\\send.avi";  //文件名
    FILE *fp = fopen(filename, "rb");  //以二进制方式打开文件
    if(fp == NULL){
        printf("Cannot open file, press any key to exit!\n");
        system("pause");
        exit(0);
    }
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    listen(servSock, 20);
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
    //循环发送数据,直到文件结尾
    char buffer[BUF_SIZE] = {0};  //缓冲区
    int nCount;
    while( (nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0 ){
        send(clntSock, buffer, nCount, 0);
    }
    shutdown(clntSock, SD_SEND);  //文件读取完毕,断开输出流,向客户端发送FIN包
    recv(clntSock, buffer, BUF_SIZE, 0);  //阻塞,等待客户端接收完毕
    fclose(fp);
    closesocket(clntSock);
    closesocket(servSock);
    WSACleanup();
    system("pause");
    return 0;
}

客户端代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int main(){
    //先输入文件名,看文件是否能创建成功
    char filename[100] = {0};  //文件名
    printf("Input filename to save: ");
    gets(filename);
    FILE *fp = fopen(filename, "wb");  //以二进制方式打开(创建)文件
    if(fp == NULL){
        printf("Cannot open file, press any key to exit!\n");
        system("pause");
        exit(0);
    }
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);
    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    //循环接收数据,直到文件传输完毕
    char buffer[BUF_SIZE] = {0};  //文件缓冲区
    int nCount;
    while( (nCount = recv(sock, buffer, BUF_SIZE, 0)) > 0 ){
        fwrite(buffer, nCount, 1, fp);
    }
    puts("File transfer success!");
    //文件接收完毕后直接关闭套接字,无需调用shutdown()
    fclose(fp);
    closesocket(sock);
    WSACleanup();
    system("pause");
    return 0;
}

在D盘中准备好send.avi文件,先运行 server,再运行 client: Input filename to save: D:\recv.avi↙ //稍等片刻后 File transfer success!

打开D盘就可以看到 recv.avi,大小和 send.avi 相同,可以正常播放。

注意 server.cpp 第42行代码,recv() 并没有接收到 client 端的数据,当 client 端调用 closesocket() 后,server 端会收到FIN包,recv() 就会返回,后面的代码继续执行。

在socket中使用域名

gethostbyname()域名获取IP

1
struct hostent *gethostbyname(const char *hostname);
  • hostname 为主机名,也就是域名。使用该函数时,只要传递域名字符串,就会返回域名对应的IP地址。
  • 返回的地址信息会装入 hostent 结构体,该结构体的定义如下:
1
2
3
4
5
6
7
struct hostent{
    char *h_name;  //official name
    char **h_aliases;  //alias list
    int  h_addrtype;  //host address type
    int  h_length;  //address lenght
    char **h_addr_list;  //address list
}

从该结构体可以看出,不只返回IP地址,还会附带其他信息,各位读者只需关注最后一个成员 h_addr_list。下面是对各成员的说明:

  • h_name:官方域名(Official domain name)。官方域名代表某一主页,但实际上一些著名公司的域名并未用官方域名注册。
  • h_aliases:别名,可以通过多个域名访问同一主机。同一IP地址可以绑定多个域名,因此除了当前域名还可以指定其他域名。
  • h_addrtype:gethostbyname() 不仅支持 IPv4,还支持 IPv6,可以通过此成员获取IP地址的地址族(地址类型)信息,IPv4 对应 AF_INET,IPv6 对应 AF_INET6。
  • h_length:保存IP地址长度。IPv4 的长度为4个字节,IPv6 的长度为16个字节。
  • h_addr_list:这是最重要的成员。通过该成员以整数形式保存域名对应的IP地址。对于用户较多的服务器,可能会分配多个IP地址给同一域名,利用多个服务器进行均衡负载。 hostent 结构体变量的组成如下图所示: http://c.biancheng.net/cpp/uploads/allimg/151110/1-151110203SY49.jpg

下面的代码主要演示 gethostbyname() 的应用,并说明 hostent 结构体的特性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main(){
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    struct hostent *host = gethostbyname("www.baidu.com");
    if(!host){
        puts("Get IP address error!");
        system("pause");
        exit(0);
    }
    //别名
    for(int i=0; host->h_aliases[i]; i++){
        printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
    }
    //地址类型
    printf("Address type: %s\n", (host->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");
    //IP地址
    for(int i=0; host->h_addr_list[i]; i++){
        printf("IP addr %d: %s\n", i+1, inet_ntoa( *(struct in_addr*)host->h_addr_list[i] ) );
    }
    system("pause");
    return 0;
}

运行结果: Aliases 1: www.baidu.com Address type: AF_INET IP addr 1: 61.135.169.121 IP addr 2: 61.135.169.125

select函数I/O多路复用

引用

1
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数含义:

  • maxfdp:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。

  • readfds:(可选)指针,指向一组等待可读性检查的套接口。

  • writefds:(可选)指针,指向一组等待可写性检查的套接口。

  • exceptfds:(可选)指针,指向一组等待错误检查的套接口。

  • timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间

    readfds数组将包括满足以下条件的套接字:

    1有数据可读

    2连接已经关闭、重设或终止

    3正在请求建立连接的套接字(listfd),此时调用accept函数会直接成功,accept相当于非阻塞的

    writefds数组包含满足下列条件的套接字:

    1有数据可以发送,此时在此sockfd上调用send,可以向对方发送数据。

    2调用connect函数,并连接成功的sockfd

返回值

  • 当返回为-1时,所有描述符集清0。
  • 当返回为0时,表示超时。
  • 当返回为正数时,表示已经准备好的描述符数。 select()返回后,在3个描述符集里,依旧是1的位就是准备好的描述符。这也就是为什么,每次用select后都要用FD_ISSET的原因。 select函数实现I/O多路复用,可以用来监视多个描述符,之后我们调用FD_ISSET函数确定具体是哪一个描述符准备好了。

timeval结构体定义如下:

1
2
3
4
5
struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};

fd_set 相关:

1
2
3
4
int FD_ZERO(fd_set *fdset);   //一个 fd_set类型变量的所有位都设为 0 (可理解为清空集合中的所有感兴趣描述符)
int FD_CLR(int fd, fd_set *fdset);  //清除某个位时可以使用 (可理解为清空集合中的某一个描述符)
int FD_SET(int fd, fd_set *fd_set);   //设置变量的某个位置位  (可理解为向集合中添加一个描述符)
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位   (可理解为判断一个描述符是否在集合中)

程序流程

  1. 绑定、监听…..
  2. 创建集合,由于调用select函数时,传入的集合参数在函数返回后可能会改变,因此创建一个集合来保存所有感兴趣的描述符allset,再创建一个集合rset用来作为select的调用参数;再创建一个数组client用来存放所有有效的描述符,并初始化各项为-1;
  3. 将监听描述符lfd加入allset,此时最大描述符maxfd = lfd;
  4. 创建while循环,将allset赋值给rset,将rset作为读集合参数,调用select函数开始阻塞等待;
  5. select函数返回后,先判断监听描述符lfd是否还存在于rset中,判断方式为if(FD_SET(lfd,&rset))。
  6. 如果判断为真,说明有新连接,则调用accept函数新连接的文件描述符并存在变量connfd中,然后再将connfd加入allset中和client数组中;
  7. 然后处理除监听描述符以外的描述符。遍历client数组,查看有效描述符是否发生了读事件,判断方式为if(FD_SET(client[i],&rset));如果判断为真,说明有数据传来,就进行read和write操作;
  8. 继续下一次循环….
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>

using namespace std;

#define SERV_IP "127.1.2.3"
#define SERV_PORT 8888
#define MAX_CONN 1024

int main()
{
    sockaddr_in servaddr,clitaddr;
    sockaddr_in clit_info[MAX_CONN];  //存放成功連接的客戶端地址信息
    int client[1024];   //存放成功連接的文件描述符
    char buf[1024];  //讀寫緩衝區
    int lfd;      //用於監聽
    int connfd;   //連接描述符
    int readyfd;  //保存select返回值
    int maxfd = 0;  //保存最大文件描述符
    int maxi = 0;  //maxi反映了client中最後一個成功連接的文件描述符的索引
    socklen_t addr_len = sizeof(clitaddr);;

    fd_set allset;  //存放所有可以被監控的文件描述符
    fd_set rset;

    FD_ZERO(&allset);//FD_ZERO(fd_set *fdset);将指定的文件描述符集清空
    FD_ZERO(&rset);

    if((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        cout<<"creat socket fault : "<<strerror(errno)<<endl;
        return 0;
    }

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERV_IP);

    if(bind(lfd,(sockaddr *)&servaddr,sizeof(servaddr)) == -1)
    {
        cout<<"bind fault : "<<strerror(errno)<<endl;
        return 0;
    }

    if(listen(lfd,128) == -1)
    {
        cout<<"listen fault : "<<strerror(errno)<<endl;
        return 0;
    }

    maxfd = lfd;  //此時只用監控lfd,因此lfd就是最大文件描述符

    //初始化client數組
    for(int i=0;i<MAX_CONN;i++)client[i] = -1;

    FD_SET(lfd,&allset);

    cout<<"Init Success ! "<<endl;
    cout<<"host ip : "<<inet_ntoa(servaddr.sin_addr)<<"  port : "<<ntohs(servaddr.sin_port)<<endl;

    cout<<"Waiting for connections ... "<<endl;

    while(1)
    {
        rset = allset ;  //rset作爲select參數時,表示需要監控的所有文件描述符集合,select返回時,rset中存放的是成功監控的文件描述符。因此在select前後rset是可能改變的,所以在調用select前將rset置爲所有需要被監控的文件描述符的集合,也就是allset
        readyfd = select(maxfd+1,&rset,NULL,NULL,NULL); //服務端只考慮讀的情況
        //執行到這裏,說明select返回,返回值保存在readyfd中,表示有多少個文件描述符被監控成功
        if(readyfd == -1)
        {
            cout<<"select fault : "<<strerror(errno)<<endl;
            return 0;
        }

        if(FD_ISSET(lfd,&rset))  //監聽描述符監控成功,說明有連接請求
        {
            int i=0;
            connfd = accept(lfd,(sockaddr *)&clitaddr,&addr_len);  //處理新連接,此時accept直接可以返回而不用一直阻塞
            if(connfd == -1)
            {
                cout<<"accept fault : "<<strerror(errno)<<endl;
                continue ;
            }
            cout<<inet_ntoa(clitaddr.sin_addr)<<":"<<ntohs(clitaddr.sin_port)<<" connected ...  "<<endl;
            //成功連接後,就將connfd加入監控描述符表中
            FD_SET(connfd,&allset);

            for(;i<MAX_CONN;i++)
            {
                if(client[i] == -1)
                {
                    client[i] = connfd;
                    clit_info[i] = clitaddr;
                    break;
                }
            }

            if(connfd>maxfd)maxfd = connfd;  //更新最大文件描述符
            if(i>maxi)maxi = i;

            readyfd --;
            if(readyfd == 0)continue;  //如果只有lfd被監控成功,那麼就重新select
        }
        //處理lfd之外監控成功的文件描述符,進行輪詢
        for(int i=0;i<=maxi;i++)
        {
            if(client[i] == -1)continue; //等於-1說明這個描述符已經無效
            if(FD_ISSET(client[i],&rset))   //在client數組中尋找是否有被監控成功的文件描述符
            {
                //此時說明client[i]對於的文件描述符監控成功,有消息發來,直接讀取即可
                int readcount = read(client[i],buf,sizeof(buf));
                if(readcount == 0)  //對方客戶端關閉
                {
                    close(client[i]);  //關閉描述符
                    FD_CLR(client[i],&allset);   //將該描述符從描述符集合中去除
                    client[i] = -1;  //相應位置置爲-1,表示失效

                    cout<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<" exit ... "<<endl;
                }
                else if(readcount == -1)
                {
                    cout<<"read fault : "<<strerror(errno)<<endl;
                    continue;
                }
                else
                {
                    cout<<"(From "<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<")";
		    for(int j=0;j<readcount;j++)cout<<buf[j];
		    cout<<endl;
                    for(int j=0;j<readcount;j++)buf[j] = toupper(buf[j]);
                    write(client[i],buf,readcount);
                }
                readyfd--;
                if(readyfd == 0)break;
            }
        }
    }
    close(lfd);
    return 0;
}

基于UDP的服务器端和客户端

sendto()发送数据

1
2
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);  //Linux
int sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen);  //Windows
  • sock:用于传输UDP数据的套接字;
  • buf:保存待传输数据的缓冲区地址;
  • nbytes:带传输数据的长度(以字节计);
  • flags:可选项参数,若没有可传递0;
  • to:存有目标地址信息的 sockaddr 结构体变量的地址;
  • addrlen:传递给参数 to 的地址值结构体变量的长度。

UDP 发送函数 sendto() 与TCP发送函数 write()/send() 的最大区别在于,sendto() 函数需要向他传递目标地址信息。

recvfrom() 接收数据

1
2
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen);  //Linux
int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen);  //Windows
  • sock:用于接收UDP数据的套接字;
  • buf:保存接收数据的缓冲区地址;
  • nbytes:可接收的最大字节数(不能超过buf缓冲区的大小);
  • flags:可选项参数,若没有可传递0;
  • from:存有发送端地址信息的sockaddr结构体变量的地址;
  • addrlen:保存参数 from 的结构体变量长度的变量地址值。

UDP回声服务器端 server.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    //创建套接字
    SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
    //绑定套接字
    sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));  //每个字节都用0填充
    servAddr.sin_family = PF_INET;  //使用IPv4地址
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址
    servAddr.sin_port = htons(1234);  //端口
    bind(sock, (SOCKADDR*)&servAddr, sizeof(SOCKADDR));
    //接收客户端请求
    SOCKADDR clntAddr;  //客户端地址信息
    int nSize = sizeof(SOCKADDR);
    char buffer[BUF_SIZE];  //缓冲区
    while(1){
        int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &clntAddr, &nSize);
        sendto(sock, buffer, strLen, 0, &clntAddr, nSize);
    }
    closesocket(sock);
    WSACleanup();
    return 0;
}

代码说明:

  1. 第12行代码在创建套接字时,向 socket() 第二个参数传递 SOCK_DGRAM,以指明使用UDP协议。

  2. 第18行代码中使用htonl(INADDR_ANY)来自动获取IP地址。

UDP回声客户端 client.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){
    //初始化DLL
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    //创建套接字
    SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);
    //服务器地址信息
    sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));  //每个字节都用0填充
    servAddr.sin_family = PF_INET;
    servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servAddr.sin_port = htons(1234);
    //不断获取用户输入并发送给服务器,然后接受服务器数据
    sockaddr fromAddr;
    int addrLen = sizeof(fromAddr);
    while(1){
        char buffer[BUF_SIZE] = {0};
        printf("Input a string: ");
        gets(buffer);
        sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&servAddr, sizeof(servAddr));
        int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &fromAddr, &addrLen);
        buffer[strLen] = 0;
        printf("Message form server: %s\n", buffer);
    }
    closesocket(sock);
    WSACleanup();
    return 0;
}

先运行 server,再运行 client,client 输出结果为:

Input a string: C语言中文网 Message form server: C语言中文网 Input a string: c.biancheng.net Founded in 2012 Message form server: c.biancheng.net Founded in 2012 Input a string:

从代码中可以看出,server.cpp 中没有使用 listen() 函数,client.cpp 中也没有使用 connect() 函数,因为 UDP 不需要连接。