一、概念
- 网络编程:编写程序使两台联网的计算机相互交换数据
- 套接字:网络数据传输所用的软件设备
- 文件描述符:Linux系统分配给文件或套接字的整数,0-2被自动分配给三种标准IO对象(stdin/stdout/stderr),文件和套接字一般要经过创建过程才会分配文件描述符,而3种标准IO对象则无需创建,会被自动分配文件描述符
- 文件句柄:Windows系统中的文件句柄对应Linux系统中的文件描述符
二、Linux网络编程
1.相关函数
- socket函数
#include<sys/socket.h>
// 功能:创建套接字
// 参数:
// domain--协议族
// type--套接字数据传输类型
// protocol--通信协议
// 返回值:成功时返回文件描述符,失败时返回 -1
int socket(int domain,int type,int protocol);
- bind函数
#include<sys/socket.h>
// 功能:给套接字分配IP地址和端口号
// 参数:
// sockfd--待分配IP地址和端口号的套接字
// sockaddr--存放IP地址和端口号等信息的结构体
// addrlen--结构体的长度
// 返回值:成功时返回 0,失败时返回 -1
int bind(int sockfd,struct sockaddr *myaddr,socklen_t addrlen);
- listen函数
#include<sys/socket.h>
// 功能:把套接字转化为可接收连接请求状态
// 参数:
// sock--希望进入等待连接请求状态的(服务器端)套接字的文件描述符
// backlog--连接请求队列的长度
// 返回值:成功时返回 0,失败时返回 -1
int listen(int sockfd,int backlog);
- accept函数
#include<sys/socket.h>
// 功能:接受连接请求
// 参数:
// sock--服务器端套接字的文件描述符
// addr--保存发起连接请求的客户端地址信息的变量地址值,调用函数后,向被填入客户端地址信息
// addrlen--存有第二个参数 addr 结构体的长度的变量的地址,调用函数后,该变量即被填入客户端地址长度
// 返回值:成功时返回文件描述符,失败时返回 -1
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
// 调用accept函数时,若等待队列为空,则 accept 函数不会返回,会进入阻塞状态,直到队列中出现新的客户端连接
- connect函数
#include<sys/socket.h>
// 功能:向服务器端发出连接请求
// 参数:
// sock --客户端套接字文件描述符
// servaddr --保存目标服务器地址信息的变量的地址值
// addrlen --以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
// 返回值:成功时返回 0,失败时返回 -1
int connect(int sockfd,struct sockaddr *serv_addr,socklen_t addrlen);
// 客户端的IP地址和端口号在调用 connect 函数时自动分配,无需调用 bind 函数
// 当服务器端接受连接请求或发生断网等异常情况而中断连接请求时,connect 函数才会返回
3.客户端-服务器端通信程序举例
功能:服务器端收到连接请求后向请求者发送"Hello world!",客户端在控制台输出收到的消息!
- 服务器端代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]) {
int serv_sock; // 服务器端套接字描述符
int clnt_sock; // 客户端套接字描述符
struct sockaddr_in serv_addr; // 保存服务器端地址信息
struct sockaddr_in clnt_addr; // 保存客户端地址信息
char message[] = "Hello world!";
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 调用 socket 函数创建套接字
// 此时的套接字不马上区分服务器端和客户端,若后面调用 bind、listen 函数,则成为服务器端套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error");
// 将 serv_addr 所指向的内存空间初始化为0
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 指定地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址初始化,利用常数 INADDR_ANY 自动获取服务器端的计算机IP地址
serv_addr.sin_port = htons(atoi(argv[1])); // 端口号初始化
// 调用 bind 函数分配IP地址和端口号
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
// 调用 listen 函数将套接字转为可接收连接状态
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
socklen_t clnt_addr_size = sizeof(clnt_addr);
// 调用 accept 函数受理连接请求
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if (clnt_sock == -1)
error_handling("accept() error");
// 调用 write 函数向客户端传递数据
write(clnt_sock, message, sizeof(message));
close(clnt_sock); // 关闭客户端套接字
close(serv_sock); // 关闭服务器端套接字
return 0;
}
- 客户端代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]) {
int sock; // 客户端套接字描述符
struct sockaddr_in serv_addr;
char message[30];
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 调用 socket 函数创建套接字
// 此时的套接字并不马上分为服务器端和客户端,若后面调用 connect 函数,则成为客户端套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("scoket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 指定地址族
serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址初始化
serv_addr.sin_port = htons(atoi(argv[2])); // 端口号初始化
// 调用 connect 函数向服务器端发出连接请求
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error");
// 调用 read 函数读取服务器端传输来的数据
int str_len = read(sock, message, sizeof(message) - 1);
if (str_len == -1)
error_handling("read() error");
printf("Message from server:%s \n", message);
close(sock); // 关闭客户端套接字
return 0;
}
运行结果:
4.基于Linux的文件操作(Linux中套接字也是看作文件)
- 打开文件
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
// 功能:以指定模式打开指定路径的文件
// 参数:
// path--文件名的字符串地址
// flag--文件打开模式信息,组合时用位或运算符“|”
// 返回值:成功时返回文件描述符,失败时返回 -1
int open(const char* path,int flag);
flag的常量值及含义:
O_CREAT 必要时创建文件
O_TRUNC 删除全部现有数据
O_APPEND 维持现有数据,在尾部添加数据
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
- 关闭文件
#include<unistd.h>
// 功能:借助文件描述符关闭文件
// 参数:
// fd--需要关闭的文件或套接字的文件描述符
// 返回值:成功时返回 0,失败时返回 -1
int close(int fd);
- 将数据写入文件
#include<unistd.h>
// 功能:将缓冲区的数据写入到文件中
// 参数:
// fd--数据传输对象的文件描述符
// buf--待传输数据的缓冲地址
// nbytes--传输数据的字节数
// 返回值:成功时返回写入的字节数,失败时返回 -1
ssize_t write(int fd,const void* buf,size_t nbytes);
- 读取文件中的数据
#include<unistd.h>
// 功能:从文件中读入数据并存入到缓冲区
// 参数:
// fd--数据接收对象的文件描述符
// buf--要接收数据的缓冲地址
// nbytes--要接收数据的最大字节数
// 返回值:成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
ssize_t read(int fd,void* buf,size_t nbytes);
- 测试代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/socket.h>
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]) {
// 测试文件描述符分配
{
int fd1, fd2, fd3;
fd1 = socket(PF_INET, SOCK_STREAM, 0);
fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC);
fd3 = socket(PF_INET, SOCK_DGRAM, 0);
printf("file descriptor 1: %d\n", fd1); // 3
printf("file descriptor 2: %d\n", fd2); // 4
printf("file descriptor 3: %d\n", fd3); // 5
close(fd1);
close(fd2);
close(fd3);
}
// 测试文件IO
{
int fd;
char buf[] = "Hello world";
char file_content[20];
fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC);
if (fd == -1)
error_handling("open() error");
if (write(fd, buf, sizeof(buf)) == -1)
error_handling("write() error");
close(fd);
fd = open("test.txt", O_RDONLY);
if (fd == -1)
error_handling("open() error");
if (read(fd, file_content, sizeof(file_content)) == -1)
error_handling("read() error");
printf("file content: %s \n", file_content);
close(fd);
}
return 0;
}
运行结果:
三、Windows网络编程
1.相关函数
- WSAStartup函数
#include<winsock2.h>
// 功能:设置程序中用到的Winsock版本,并初始化相应版本的库
// 参数:
// wVersionRequested--要用的 Winsock 版本信息
// lpWSAData--WSADATA结构体变量的地址值
// 返回值:成功时返回 0,失败时返回非零的错误代码值
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
借助MAKEWORD宏构建WORD型版本信息
MAKEWORD(1,2); // 主版本号为1,副版本号为2,返回0x0201
MAKEWORD(2,2); // 主版本号为2,副版本号为2,返回0x0202
- WSACleanup函数
#include<winsock2.h>
// 功能:注销Winsock库
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int WSACleanup();
- socket函数
#include<winsock2.h>
// 功能:创建套接字
// 返回值:成功时返回套接字句柄,失败时返回 INVALID_SOCKET
SOCKET socket(int af,int type,int protocol);
- bind函数
#include<winsock2.h>
// 功能:给套接字分配IP地址和端口号
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int bind(SOCKET s,const struct sockaddr* name,int namelen);
- listen函数
#include<winsock2.h>
// 功能:把套接字转化为可接收连接请求状态
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int listen(SOCKET s,int backlog);
- accept函数
#include<winsock2.h>
// 功能:接受连接请求
// 参数:成功时返回套接字句柄,失败时返回 INVALID_SOCKET
SOCKET accept(SOCKET S,struct sockaddr* addr,int *addrlen);
- connect函数
#include<winsock2.h>
// 功能:向服务器端发出连接请求
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int connect(SOCKET s,const struct sockaddr* name,int namelen);
- closesocket函数
#include<winsock2.h>
// 功能:关闭套接字
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int closesocket(SOCKET s);
2.visual studio链接ws2_32.lib
- 快捷键 Alt+F7-》配置属性-》连接器-》输入-》附加依赖项-》下拉菜单点击编辑-》空白处输入ws2_32.lib-》确定!两个项目都要设置
- 另一种做法是在文件头加入代码:#pragma comment(lib,"ws2_32.lib"),这是告诉编译器在编译形成的.obj文件和.exe文件中加一条信息,使得链接器在链接库的时候要去找的 wsock32.lib 这个库,把它加入工程中,而不是动态载入 ws2_32.dll。
3.客户端-服务器端通信程序举例
功能:服务器端向连接请求者发送"Hello world!",客户端在控制台输出收到的消息!
- 服务器端项目代码
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
char message[] = "Hello world!";
if (argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
// 调用 WSAStartup 函数初始化 Winsock 库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
// 调用 socket 函数创建服务器端套接字
hServSock = socket(PF_INET, SOCK_STREAM, 0);
if (hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
// 调用 bind 函数分配IP地址和端口
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
// 调用 listen 函数将套接字转为可接受连接状态
if (listen(hServSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
int szClntAddr = sizeof(clntAddr);
// 调用 accept 函数受理客户端连接请求
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
if (hClntSock == INVALID_SOCKET)
ErrorHandling("listen() error");
// 调用 send 函数向客户端发送数据
send(hClntSock, message, sizeof(message), 0);
closesocket(hClntSock);
closesocket(hServSock);
WSACleanup();
return 0;
}
- 客户端项目代码
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
#pragma warning(disable:4996)
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 调用 WSAStartup 函数初始化 Winsock 库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
// 调用 socket 函数创建客户端套接字
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]); // 有warning
servAddr.sin_port = htons(atoi(argv[2]));
// 调用 connect 函数向服务器发出连接请求
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error");
// 调用 recv 函数读取服务器发送来的数据
int strlen = recv(hSocket, message, sizeof(message) - 1, 0);
if (strlen == -1)
ErrorHandling("read() error");
printf("Message from server: %s\n", message);
closesocket(hSocket);
WSACleanup();
return 0;
}
运行两个项目的方法:
- 右击解决方案-》属性-》选择多个启动项目-》下拉选择启动
- 设置main函数参数
- Ctrl+F5运行,运行结果:
- 另一个运行方法:分别运行两个项目,然后找到两个项目的可执行文件,分别在两个cmd窗口运行
4.基于Windows的I/O函数(Windows区分文件和套接字)
- send函数
#include<winsock2.h>
// 功能:向指定套接字写入缓冲区的数据
// 返回值:成功时返回传输字节数,失败时返回 SOCKET_ERROR
int send(SOCKET s,const char* buf,int len,int flags);
- recv函数
#include<winsock2.h>
// 功能:从套接字读入数据并保存到缓冲区中
// 返回值:成功时返回接收到的字节数(收到EOF时为0),失败时返回 SOCKET_ERROR
int recv(SOCKET s,const char* buf,int len,int flags);
参考书籍:《TCP/IP网络编程》尹圣雨 著,金果哲 译