老林的C语言新课, 想快速入门点此
我们学完了 TCP / IP 相关知识, 但是如何使用呢. 所以下一步我们介绍用 C 语言的 socket API编写简单程序进行网络通信.
Socket编程是一种用于网络通信的编程技术。它允许在不同的计算机之间建立TCP/IP连接,并在这些计算机之间传输数据。
使用套接字(socket)建立网络连接, 它提供了一种通用的接口,可以在不同的计算机之间建立连接,并传输数据。
有两种类型:服务器套接字和客户端套接字。服务器套接字用于监听来自客户端的连接请求,并接受这些连接。客户端套接字则用于向服务器发起连接请求,并发送数据。
Socket编程的主要步骤包括创建套接字、绑定套接字、监听连接请求、接受连接、发送和接收数据等。它可以用于构建各种类型的网络应用程序,如网站、聊天室、文件传输等。
C语言的socket API是一组用于网络通信的函数库(Linux在库文件
socket():创建一个新的套接字,并返回其描述符。
bind():将套接字与一个本地地址绑定。
listen():将套接字转换为被动模式,等待连接请求。
accept():接受一个连接请求,并返回一个新的套接字描述符。
connect():向远程主机发起连接请求。
send():向套接字发送数据。
recv():从套接字接收数据。
close():关闭套接字。
函数原型:
int socket(int __domain, int __type, int __protocol);
int bind(int __fd, const struct sockaddr *__addr, socklen_t __len);
int listen(int __fd, int __n);
int accept(int __fd, struct sockaddr *__addr, socklen_t *__addr_len);
int connect(int __fd, const struct sockaddr *__addr, socklen_t __len);
ssize_t send(int __fd, const void *__buf, size_t __n, int __flags);
ssize_t recv(int __fd, void *__buf, size_t __n, int __flags);
以上是常用的socket API函数。
先了解一下主机字节序和网络字节序:
主机字节序是指计算机处理数据时采用的字节序,而网络字节序是指在网络传输数据时所采用的字节序。
主机字节序有两种,即大端字节序和小端字节序。
大端字节序是指将数据的高位字节存放在内存的低地址处,而小端字节序则是将数据的低位字节存放在内存的低地址处。
网络字节序采用的是大端字节序,这是因为网络传输数据时需要保证数据在不同主机之间的传输是可靠的,而不同的主机可能采用不同的主机字节序,因此采用统一的网络字节序可以保证数据在不同主机之间的传输正确无误.
#include
#include
void byteOrder();
int main()
{
byteOrder();
return 0;
}
void byteOrder()
{
union
{
short value;
char unionBytes[sizeof(short)];
} test;
test.value = 0x0102;
if ((test.unionBytes[0] == 1) && (test.unionBytes[1] = 2))
{
printf("big endian\n");
}
else if ((test.unionBytes[0] == 2) && (test.unionBytes[1] == 1))
{
printf("little endian\n");
}
else
{
printf("unknown...\n");
}
}
#include
#include
#include
int main()
{
unsigned int test = htonl(0x11223344); // 主机转网络long
printf("test = 0x11223344, htonl(test) = 0x%x\n", test);
test = ntohl(0x44332211); // 网络转主机long
printf("test = 0x44332211, ntohl(test) = 0x%x\n", test);
unsigned short tests = htons(0x1122); // 主机转网络short
printf("test = 0x1122, htons(test) = 0x%x\n", tests);
tests = ntohs(0x2211); // 网络转主机short
printf("test = 0x2211, ntohl(test) = 0x%x\n", tests);
return 0;
}
htonl()是主机转网络long, ntohl()是网络转主机long, htons()是主机转网络short, ntohs是网络转主机short, 长整型函数通常用来转换IP地址, 短整型函数用来转换端口号.
socket API中参数有一个类型sockaddr, 即socket地址, 其结构如下:
/* Structure describing a generic socket address. */
struct sockaddr
{
sa_family_t sa_family; /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
这个类型满足 AF_INET地址族, 即TCP/IPv4, 也是目前最常用的地址族.
我们一般喜欢看 255.255.255.192 这类地址, 但计算机看的是 0xff ff ff c0(小端,需转成大端) 这类地址, 二者可以转换.
in_addr_t inet_addr(const char *__cp); // 将点字符地址转成整型地址
char *inet_ntoa(struct in_addr __in); // 将整型地址(struct in_addr结构)转成点字符地址
int inet_aton(const char *__cp, struct in_addr *__inp); // 将点字符地址转成整型地址(struct in_addr结构)
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr; // 32位整型
};
#include
#include
#include
#include
#include
int main()
{
char IP[] = "255.255.255.192";
uint32_t ipNum = inet_addr(IP); // 点字符地址转整型地址, 大端
ipNum = ntohl(ipNum); // 转成小端
printf("0x%x\n", ipNum); // 0xffffffc0
struct in_addr ipAdd; // 声明IPv4地址结构
ipAdd.s_addr = htonl(0xffffff80); // 转换位大端
printf("0x%x\n", ipAdd.s_addr); // 0x80ffffff
strcpy(IP, inet_ntoa(ipAdd)); // 将整型地址转为点字符地址并拷贝给IP字符数组,
// 因函数使用静态字符数组存储地址, 不可用指针获取地址, 否则容易丢失
printf("%s\n", IP); // 255.255.255.128
ipAdd.s_addr = 0; // 将整型地址置零
inet_aton(IP, &ipAdd); // 将IP数组的点字符地址转成整型地址
printf("0x%x\n", ipAdd.s_addr); // 0x80ffffff (大端)
return 0;
}
// af 是地址族 AF_INET 或 AF_INET6, 对应IPv4和IPv6
// cp 是点字符的字符数组, 需要足够大的空间, IPv4至少16字节,IPv6至少46字节
// buf 是 struct in_addr 或 struct in6_addr 指针,对应IPv4和IPv6
int inet_pton(int __af, const char *__cp, void *__buf);
// af 是地址族 AF_INET 或 AF_INET6, 对应IPv4和IPv6
// cp 是 struct in_addr 或 struct in6_addr 指针,对应IPv4和IPv6
// buf 是点字符的字符数组, 需要足够大的空间
// len 是 INET_ADDRSTRLEN 或 INET6_ADDRSTRLEN, 即字符数组空间, IPv4至少16字节,IPv6至少46字节
const char *inet_ntop(int __af, const void *__cp, char *__buf, socklen_t __len);
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#ifdef __USE_MISC
#define s6_addr16 __in6_u.__u6_addr16
#define s6_addr32 __in6_u.__u6_addr32
#endif
};
#include
#include
#include
#include
#include
int main()
{
char IP[INET6_ADDRSTRLEN] = "fe80::ab1c:54f9:dcaf:e8b4";
struct in6_addr ipv6Add;
inet_pton(AF_INET6, IP, &ipv6Add);
for (int i = 0; i < 16; i++)
{
printf("%02x ", ipv6Add.s6_addr[i]);
}
printf("\n");
char IPv6[INET6_ADDRSTRLEN] = "";
inet_ntop(AF_INET6, &ipv6Add, IPv6, INET6_ADDRSTRLEN);
printf("%s\n", IPv6);
return 0;
}
本示例是基于Windows环境做客户端, Linux环境做服务端, Windows环境与Linux环境的socket编程稍有不同
头文件Windows要用
程序结尾要用closesocket(sock)关闭套接字sock, 然后需要关闭DLL, 通过WSACleanup()函数.
#include
#include
#include
#include
#include
int main()
{
// 初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 创建 socket
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
{
printf("Could not create socket");
}
// 设置服务器地址和端口
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr("172.31.94.213");
server.sin_family = AF_INET;
server.sin_port = htons(8080);
// 连接服务器
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
perror("connect failed. Error");
return 1;
}
printf("Connected to server\n");
char message[1000] = "";
char server_reply[2000] = "";
while (1)
{
printf("Enter message : ");
scanf("%s", message);
// 发送数据
if (send(sock, message, strlen(message), 0) < 0)
{
puts("Send failed");
return 1;
}
// 接收服务器的响应
if (recv(sock, server_reply, 2000, 0) < 0)
{
puts("recv failed");
break;
}
puts(server_reply);
}
// 关闭socket, Windows有自己的函数,Linux用colse即可
closesocket(sock);
// 终止使用 DLL
WSACleanup();
return 0;
}
首先,在Windows环境我们初始化DLL, 然后创建一个 socket,使用 socket() 函数,它的第一个参数是地址族(如 AF_INET 表示 IPv4),第二个参数是套接字类型(如 SOCK_STREAM 表示 TCP),第三个参数是协议(如 0 表示默认协议)。
然后,我们设置服务器地址和端口,使用 struct sockaddr_in 结构体表示。inet_addr() 函数将字符串形式的 IP 地址转换为网络字节序的整数形式。
接下来,我们使用 connect() 函数连接服务器。如果连接成功,该函数返回 0,否则返回 -1。
在循环中,我们使用 send() 函数发送数据到服务器,使用 recv() 函数接收服务器的响应。
最后,我们关闭 socket,使用 close() 函数, 并终止DLL。
编译参数需要加上库 lws2_32:
E:\msys64\clang64\bin\clang.exe -glldb -lws2_32 learnSocket_06*.c -o E:\clangC++\answer\C\learnSocket_06.exe
// 服务器端代码
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 创建一个 socket 文件描述符
int server_fd = 0;
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 socket 选项
// 打开地址复用, 端口复用, 通知内核,如果端口忙,但TCP状态位于 TIME_WAIT,可以重用端口。
// 要慎用, 如未来得及完成四次挥手, 容易引起错误.
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)))
{
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 绑定 socket 到指定的 IP 和端口
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 自身地址,同 inet_addr("172.31.94.213")
address.sin_port = htons(8080);
// 将address的地址及端口与server_fd绑定
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听端口server_fd
// 内核在开始拒绝连接请求前, 队列中要排队的未完成的连接请求的数量是3
// 通常会设置较大值,比如1024, 但演示就设小点, 不浪费资源了.
if (listen(server_fd, 3) < 0)
{
perror("listen");
exit(EXIT_FAILURE);
}
// 等待客户端连接, 直至连接请求到达server_fd监听描述符
// 将客户端IP地址写入address, 返回一个已连接描述符client_fd
// 此过程是可并发的, 即一个监听描述符可对应多个已连接描述符
int client_fd = 0;
unsigned int addrlen = sizeof(address);
if ((client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0)
{
perror("accept");
exit(EXIT_FAILURE);
}
// 从客户端读取数据
char buffer[1024] = "";
if (recv(client_fd, buffer, 1024, 0))
{
printf("%s\n", buffer);
}
// 向客户端发送数据
char hello[128] = "Hello from server";
send(client_fd, hello, strlen(hello), 0);
printf("Hello message sent\n");
// 关闭socket, 实际是将socket描述符的引用计数减一
// 对于要立即终止连接, 可以用 int shutdown(int sockfd, int howto);
// howto可以是SHUT_RD关闭读, SHUT_WR关闭写, SHUT_RDWR关闭读写
close(client_fd);
close(server_fd);
return 0;
}
与客户端不同, 服务端会多了两步: bind, listen, 建立一个监听socket, 通过accept等待连接, 当与连接后, 则继续下一步读取客户端数据, 返回一些数据, 然后关闭连接.
具体试验时, 我们先运行服务端, 使其处于监听状态, 然后运行客户端, 连接服务端, 发送一些字符, 并接收服务端返回的字符, 完成后关闭socket, 结束程序.
socket编程说简单则简单, 只有固定的几个API就可以进行简单的连接通讯.
但说难也难, 这中间涉及计算机系统, TCP\IP协议及其各种设置, 各种不同状态.
如果要满足高并发, 还要配合多线程编程, 这涉及的就更多, 可以写几本书了.
好在伟大的计算机科学网络导师, 理查德.史蒂文斯已经有多部著作进行阐述, 欲深入了解, 可以继续阅读:
《UNIX环境高级编程》《TCP/IP详解 卷1:协议》《UNIX网络编程 卷1》《UNIX网络编程 卷2》
老林的C语言新课, 想快速入门点此