C语言实现UDP服务器

1 TCP/IP协议与Winsock网络编程接口

为了方便网络编程,20世纪90年代初,Microsoft联合其他几家公司共同制定了一-套Windows下的网络编程接口,即Winsock规范。它不是一种网络协议,而是-一套开放的、支持多种协议的Windows下的网络编程接口。Winsock可以访问很多种网络协议,可以把它当作﹒些协议的封装。现在的Winsock已经基本上实现了与协议无关。可以使用Winsock束调用多种协议的功能。

那么,Winsock和TCP/IP协议到底有什么关系呢?实际上,Winsock就是TCP/IP协议的种封装。你可以通过调用Winsock的接口函数来调用TCP/IP的各种功能。例如,我们想用TCP/IP协议发送数据,就可以使用Winsock的接口函数send()来调用TCP/IP的发送数据功能,Winsock已经封装好了发送数据的功能。

2 Winsock 的常用API

2.1 WSAStartup函数

WSAStartup函数的格式如下

1
int WSastartup( WORD wversionRequested,LPWSADATA 1pwSAData);

程序在使用Socket之前必须调用WSAStartup函数。

  • 该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明修订版本、低位字节指明主版本;

  • 操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库、然后将找到的Socket库绑定到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其他Socket函数了。

该函数执行成功后返回0。

2.2 socket函数

socket函数的格式为:

1
Socket socket( int af, int type, int protocol );

应用程序调用socket函数来创建一个能够进行网络通信的套接字。

  • 第一个参数指定应用程序使用的通信协议的协议族,如果使用TCP/IP协议族,则该参数置PF_INET,设置通信域(本地(PF_LOCAL),ipv4(PF_INET),ipv6(PF_INET6)。

  • 第二个参数指定要创建的套接字类型(流套接字类型为SOCK_STREAM,数据报套接字类型为SOCK_DGRAM)。

  • 第三个参数指定应用程序所使用的通信协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

如果该函数调用成功,则返回新创建的套接字的描述符,如果失败则返回INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表用一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。

2.3 bind函数

bind函数的格式为:

1
int bind( SOCKET s,const struct sockaddr FAR *name, int namelen);

当创建了一个Socket以后,套接字数据结构中有一个默认的IP地址和默认的端口号。一个服务程序必须调用bind函数来给Socket绑定一个IP地址和一个特定的端口号。客户程序一般不必调用bind函数来为其Socket绑定IP地址和端口号。该函数的第一个参数指定待绑定的Socket描述符;第二个参数指定一个sockaddr结构,该结构的定义如下:

1
2
3
4
5
6
struct sockaddr
{
u _short sa_family;

char sa_data[ 14 ];
};

sa_family指定地址族,对于TCP/IP协议族的套接字,将其置AF_INET。当对TCP/IP协议族的套接字进行绑定时,我们通常使用如下所示的另一个地址结构:

1
2
3
4
5
6
7
8
9
10
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填充
};

其中,sin_family置AF_INET; sin_port指明端口号; sin_addr结构体中只有一个唯一的字段s_addr,它表示IP地址。该字段是一个整数,一般用函数inet_addr()把字符串形式的IP地址转换成无符号长整型的整数值后再赋给s_addr。我们用0来填充sin_zero数组,目的是让sockaddr_in结构的大小与sockaddr结构的大小一致。

1
2
3
struct in_addr{
in_addr_t s_addr; //32位的IP地址
};

s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换将一个无符号短整型数值转换为网络字节序,即大端模式。

2.4 recvfrom函数

recefrom为无连接读函数,函数格式为:

1
int recvfrom(int sockfd, void *buf, int buf_len, unsigned int flags,struct sockaddr *from,int fromlen);

从UDP接收数据,返回实际接收的字节数,失败时返回-1

  • Sockfd:套接字描述符

  • buf:指向内存块的指针

  • buf_len:内存块大小,以字节为单位

  • flags:一般为0

  • from:远端的地址,IP地址和端口号

  • fromlen:远端地址长度

举例:recvfrom(sockfd,buf,8192,0,(struct sockaddr *)&address, sizeof(address));

2.5 sendto函数

sendto为无连接函数,函数格式为:

1
int sendto(int sockfd, const void * data, int data_len, unsigned int flags, struct sockaddr *remaddr,int remaddr_len)

基于UDP发送数据报,返回实际发送的数据长度,出错时返回-1

  • sockfd:套接字描述符

  • data:指向要发送数据的指针

  • data_len:数据长度

  • flags:通常为0

  • remaddr:远端地址:IP地址和端口号

  • remaddr_len :地址长度

举例:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&address, sizeof(address));

2.6 closesocket函数

closesocket函数的格式为

1
int closesocket ( SOCKET s ) ;

closesocket函数用来关闭一个描述符为s的套接字。每个进程中都有一个套接字描述符表,表中的每个套接字描述符都与一个位于操作系统缓冲区中的套接字数据结构相对应,有可能有几个套接字描述符指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构被引用的次数,即有多少个套接字描述符指向该结构。closesocket函数如果执行成功就返回0,否则返回SOCKET_ERROR。

2.7 WSACleanup函数

WSACleanup函数的格式为

1
int WSAcleanup ( void ) ;

应用程序在完成对请求的Socket库的使用后,要调用WSACleanup函数来解除与Socket库的绑定并且释放Socket库所占用的系统资源。

3 代码实现

3.1 服务端代码实现

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
#include<stdio.h>
#include<winsock2.h>//为socket2.0

#define BUF_SIZE 255

int main(int argc,char *argv[]) {

//返回一个无符号16位整形数,高八位(Socket修订版本)和低八位(主版本)
WORD socketVersion=MAKEWORD(2,2);

//请求的Socket库,这个结构被用来存储被WSAStartup函数调用后返回的Windows Sockets数据
WSADATA wsadata;

SOCKET serveSocket;

struct sockaddr_in serveAddr;

struct sockaddr_in clientAddr;

int len=-1;

int addrLen=sizeof(clientAddr);

//缓冲数据
char receiveData[BUF_SIZE];

char sendData[BUF_SIZE];

//初始化socket库
if(WSAStartup(socketVersion,&wsadata)!=0) {
printf("socket库初始化失败\n");
return 0;
}

//创建socket
serveSocket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

if(serveSocket==INVALID_SOCKET) {
printf("socket服务器创建失败");
return 0;
}

//设置地址类型、端口号、接收地址
serveAddr.sin_family=AF_INET;
serveAddr.sin_port=htons(15632);
//监听本机的所有接口
serveAddr.sin_addr.S_un.S_addr= htonl(INADDR_ANY);

//socket绑定设置的IP和端口号
if(bind(serveSocket,(SOCKADDR*)&serveAddr,sizeof(serveAddr))==SOCKET_ERROR) {
printf("绑定IP和端口出现问题\n");
return 0;
}

printf("服务器开始运行\n");

//接收数据
while(true) {
len=recvfrom(serveSocket,receiveData,BUF_SIZE-1,0,(SOCKADDR*)&clientAddr,&addrLen);
if(len>0) {
receiveData[len]=0x00;
printf("接收到一个连接,IP地址为:%s \n",inet_ntoa(clientAddr.sin_addr));
printf("客户端:%s \n",receiveData);
}

//响应数据
printf("服务器:");
fgets(sendData,sizeof(sendData),stdin);//标准流
sendData[strlen(sendData)-1]='\0';
sendto(serveSocket,sendData,strlen(sendData),0,(SOCKADDR*)&clientAddr,addrLen);
}

closesocket(serveSocket);
WSACleanup();
return 0;

}

3.2 客户端代码实现

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
#include<stdio.h>
#include<winsock2.h>

#define BUF_SIZE 255

int main(int argc,char *argv[]) {

WORD socketVersion=MAKEWORD(2,2);

WSADATA wsadata;

SOCKET clientSocket;

struct sockaddr_in serveAddr;

int len=-1;

int addrLen=sizeof(serveAddr);


//缓冲数据
char receiveData[BUF_SIZE];

char sendData[BUF_SIZE];

if(WSAStartup(socketVersion,&wsadata)!=0) {
printf("socket库初始化失败\n");
return 0;
}

//创建socket
clientSocket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

if(clientSocket==INVALID_SOCKET) {
printf("socket客户端创建失败\n");
return 0;
}

if(argc==5) {
struct sockaddr_in clientAddr;
clientAddr.sin_family=AF_INET;
clientAddr.sin_addr.S_un.S_addr=inet_addr(argv[2]);
clientAddr.sin_port=htons(atoi(argv[3]));
if(bind(clientSocket,(SOCKADDR*)&clientAddr,sizeof(clientAddr))==SOCKET_ERROR) {
printf("绑定IP和端口出现问题\n");
return 0;
}
serveAddr.sin_family=AF_INET;
serveAddr.sin_port=htons(atoi(argv[5]));
serveAddr.sin_addr.S_un.S_addr=inet_addr(argv[4]);
} else {
//设置地址类型、端口号、接收地址
serveAddr.sin_family=AF_INET;
serveAddr.sin_port=htons(15632);
serveAddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
}


//收发数据
while(true) {
//发送数据
printf("客户端:");
fgets(sendData,sizeof(sendData),stdin);
sendData[strlen(sendData)-1]='\0';
//不再发送数据
if(strcmp(sendData,"bye") == 0)
break;

printf("send OK!\n");


sendto(clientSocket,sendData,strlen(sendData),0,(SOCKADDR*)&serveAddr,addrLen);
//接收数据
len=recvfrom(clientSocket,receiveData,BUF_SIZE-1,0,(SOCKADDR*)&serveAddr,&addrLen);
if(len>0) {
receiveData[len]=0x00;
printf("服务器:%s \n",receiveData);
}

}

closesocket(clientSocket);
WSACleanup();
return 0;

}

4 遇到的问题

4.1 undefined reference to’WSAStartup’相关编译错误

打开dev-C++的工具->编译选项,添加-lws2_32命令即可解决,如下图所示。

image-20221228132347388

参考资料

基于UDP的服务器端和客户端 (biancheng.net)

Dev C++的undefined reference to ‘__imp_htons’或codeclock的undefined reference to’WSAStartup to@8’相关编译器错


C语言实现UDP服务器
https://pursuemilk.github.io/2022/12/28/C语言实现UDP服务器/
作者
PursueMilk
发布于
2022年12月28日
许可协议