理解网络编程和套接字
网络编程就是编写程序让两个计算机通过网络进行数据交互,不需要关注计算机之间是通过什么方式连接,也不需要关注传输数据的软件是什么,关注的是如何将让两个计算机建立连接以及怎么传输数据。
常见的网络编程有两种套接字:TCP 套接字和 UDP 套接字。下面通过一个简单的 TCP 套接字程序理解 tcp 服务器端和客户端的编程步骤。
TCP 服务器端基本流程
tcp 服务器端在 Linux 中一般有四步,要是再 Windows 中则要多几步,多的这几步仅限对库的使用。先看 Linux 和 Windows 两个共有的步骤:
- 创建套接字 ——> 可以简单的理解为买了一个手机,没有手机无法通信
- Linux 中的接口函数
#include <sys/types.h> #include <sys/socket.h> // 成功返回文件描述符,失败返回 -1 int socket(int domain, int type, int protocol);
- Windows 中的接口函数
#include <winsock2.h> // 成功返回套接字,失败返回 INVALID_SOCKET SOCKET socket(int af, int type, int protocol);
- 给套接字绑定地址信息 ——> 获取手机号需要绑定你的身份信息
- Linux 中的接口函数
#include <sys/types.h> #include <sys/socket.h> // 成功返回 0,失败返回 -1 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- Windows 中的接口函数
#include <winsock2.h> // 成功返回 0,失败返回 SOCKET_ERROR int bind(SOCKET s, const struct sockaddr *addr, socklen_t addrlen);
- 将套接字设置为接收连接的状态 ——> 手机中装上手机卡后,需要确保手机处于开机状态并且有信号,别人才可以打电话进来
- Linux 中的接口函数
#include <sys/types.h> #include <sys/socket.h> // 成功返回 0,失败返回 -1 int listen(int sockfd, int backlog);
- Windows 中的接口函数
#include <winsock2.h> // 成功返回 0,失败返回 SOCKET_ERROR int listen(SOCKET s, int backlog);
- 接收连接请求 ——> 按下接听按钮
- Linux 中的接口函数
#include <sys/types.h> #include <sys/socket.h> // 成功返回非负整数,这个整数是客户端的文件描述符,失败返回 -1 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- Windows 中的接口函数
#include <winsock2.h> // 成功返回非负整数,这个整数是客户端的套接字,失败返回 SOCKET_ERROR SOCKET accept(SOCKET s, struct sockaddr *addr, socklen_t *addrlen);
上述四个步骤是编写 TCP 服务器端的一个基本框架,一步都不能少。如果使用 Windows 编写网络相关的程序,必须使用 winsock2.h
库,并且在编译的时候需要链接 ws2_32
。因此 Windows 网络编程代码中需初始化此库,并且在结束的时候还需要注销此库,函数如下
#include <winsock2.h>
// 成功返回 0,失败返回非 0 的错误代码值
int WSAStartup(WORD wVersionRequested, LPWSDATA lpWSAData);
// WORD 表示 winsock 的版本类型,直接传递则需要使用十六进制表示,高 8 位为副版本号,低八位为主版本号,如 0x0102
// 为了方便可以使用 MAKEWORD 函数,只需要两个参数,主版本号和副版本号,如 MAKEWORD(2, 1);
// 第二个参数就是一个 WSADATA 结构体变量的地址,将其传入即可
int WSACleanup(void);
除此之外,Windows 网络编程中关闭套接字也是使用不一样的函数
#include <winsock2.h>
// 成功返回 0,失败返回 SOCKET_ERROR
int closesocket(SOCKET s);
TCP 服务器端代码实现
Linux 中的代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (2 != argc) {
fprintf(stderr, "Usage: %s <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
perror("socket() error");
exit(EXIT_FAILURE);
}
// 2. 绑定地址信息
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (-1 == bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
perror("bind() error");
close(sockfd);
exit(EXIT_FAILURE);
}
// 3. 打开可连接状态,进入监听
if (-1 == listen(sockfd, 5)) {
perror("listen() error");
close(sockfd);
exit(EXIT_FAILURE);
}
struct sockaddr_in clnt_addr;
memset(&clnt_addr, 0, sizeof(clnt_addr));
socklen_t clnt_len = sizeof(clnt_addr);
int clntfd = accept(sockfd, (struct sockaddr *)&clnt_addr, &clnt_len);
if (-1 == clntfd) {
perror("accept() error");
close(sockfd);
exit(EXIT_FAILURE);
}
char message[] = "Hello World!";
write(clntfd, message, sizeof(message));
close(clntfd);
close(sockfd);
return 0;
}
Windows 中的代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
void error_handling(const char *msg);
int main(int argc, char *argv[]) {
if (2 != argc) {
fprintf(stderr, "Usage: %s <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 初始化 winsock 库
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// 1. 创建套接字
SOCKET serv_sock = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == serv_sock)
error_handling("socket error");
// 2. 绑定本地信息
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (-1 == bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
closesocket(serv_sock);
error_handling("bind error");
}
// 3. 打开可连接状态
if (-1 == listen(serv_sock, 5)) {
closesocket(serv_sock);
error_handling("listen error");
}
// 4. 接收客户端的连接
struct sockaddr_in clnt_addr;
int clnt_len = sizeof(clnt_addr);
SOCKET clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_len );
if (-1 == clnt_sock) {
closesocket(serv_sock);
error_handling("accept error");
}
char message[] = "hello world!";
int size = send(clnt_sock, message, sizeof(message), 0);
if (-1 == size) {
closesocket(serv_sock);
error_handling("send error");
}
closesocket(clnt_sock);
closesocket(serv_sock);
// 注销 winsock 库
WSACleanup();
return 0;
}
void error_handling(const char *msg) {
fputs(msg, stderr);
fputc('\n', stderr);
exit(EXIT_FAILURE);
}
TCP 客户端的基本流程和代码实现
TCP 客户端一般只有两步,如果在 Windows 中编写,还是需要加上库的初始化和最后的注销:
- 创建套接字 ——> 购买一个手机
- 请求连接 ——> 拨打电话
- Linux 中的接口实现:
#include <sys/types.h> #include <sys/socket.h> // 成功返回 0,失败返回 -1 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- Windows 中的接口实现:
#include <winsock2.h> // 成功返回 0,失败返回 SOCKET_ERROR int connect(SOCKET s, const struct sockaddr *addr, socklen_t addrlen);
TCP 客户端代码实现
Linux 中的代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (3 != argc) {
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 创建套接字
int clntfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == clntfd) {
perror("socket() error");
exit(EXIT_FAILURE);
}
// 2. 发送连接请求
struct sockaddr_in clnt_addr;
memset(&clnt_addr, 0, sizeof(clnt_addr));
clnt_addr.sin_family = AF_INET;
clnt_addr.sin_addr.s_addr = inet_addr(argv[1]);
clnt_addr.sin_port = htons(atoi(argv[2]));
if (-1 == connect(clntfd, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr))) {
perror("connect() error");
close(clntfd);
exit(EXIT_FAILURE);
} else {
printf("Connected ......\n");
}
char message[1024] = {0};
int len = read(clntfd, message, 1024);
if (-1 == len) {
perror("read() error");
close(clntfd);
exit(EXIT_FAILURE);
}
printf("read message from server: %s\n", message);
close(clntfd);
return 0;
}
Windows 中的代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#define BUFFERSIZE 1024
void error_handling(const char *msg);
int main(int argc, char *argv[]) {
if (3 != argc) {
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 初始化 winsock 库
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// 1. 创建套接字
SOCKET clnt_sock = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == clnt_sock)
error_handling("socket error");
// 2. 向服务器发送连接请求
struct sockaddr_in clnt_addr;
memset(&clnt_addr, 0, sizeof(clnt_addr));
clnt_addr.sin_family = AF_INET;
clnt_addr.sin_addr.s_addr = inet_addr(argv[1]);
clnt_addr.sin_port = htons(atoi(argv[2]));
if (-1 == connect(clnt_sock, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr))) {
closesocket(clnt_sock);
error_handling("connect error");
}
char buffer[BUFFERSIZE] = {0};
int len = recv(clnt_sock, buffer, BUFFERSIZE, 0);
if (-1 == len) {
closesocket(clnt_sock);
error_handling("recv error");
}
printf("buffer from server: %s\n", buffer);
closesocket(clnt_sock);
// 注销 winsock 库
WSACleanup();
return 0;
}
void error_handling(const char *msg) {
fputs(msg, stderr);
fputc('\n', stderr);
exit(EXIT_FAILURE);
}
分别编译运行上述两个程序,先启动服务端的程序,再启动客户端的程序。此时客户端会收到服务端发来的数据,然后两个程序都会立即退出。
上述 Linux 实现的服务器端和客户端都使用 read
和 write
函数,是因为在 Linux 中,一切都是文件,socket 也是文件,因此可以使用文件相关的读写操作。而在 Windows 中,网络套接字和文件是有区别的,需要使用网络读写专用的函数 recv
和 send
来进行操作,这两个函数在 Linux 中也适用。