书中源码自取
https://gitee.com/pipe-man/tcp_ip_socket/tree/master/%E6%BA%90%E4%BB%A3%E7%A0%81
1.1 理解网络编程和套接字
网络编程中接受连接请求的套接字创建过程可整理如下:
#include <sys/socket.h>
(以下函数:成功返回0或文件描述符,失败返回-1)
- 构建接电话套接字
- 调用socket函数创建套接字
int socket(int domain, int type, int protocol)
- 调用bind函数分配IP和端口号
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen)
- 调用listen函数转为可接受请求状态
int listen(int sockfd, int backlog)
- 调用accept函数受理连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
- 调用socket函数创建套接字
//server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void ErrHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]) {
int serv_sock;
int client_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in client_addr;
socklen_t client_addr_size;
char message[] = "hello world!";
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
//1.调用socket函数创建套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1) {
ErrHandling("socket() err");
}
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]));
//2.调用bind函数分配ip和port
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
ErrHandling("bind() error");
}
//3.调用listen将套接字转为可接收状态
if (listen(serv_sock, 5) == -1) {
ErrHandling("listen() error");
}
client_addr_size = sizeof(client_addr);
//4.调用accept受理连接请求
client_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &client_addr_size);
if (client_sock == -1) {
ErrHandling("accept() error");
}
//传输数据
write(client_sock, message, sizeof(message));
close(client_sock);
close(serv_sock);
return 0;
}
- 构建打电话套接字
- 调用socket int socket(int domain, int type, int protocol)
- 调用connect()发送连接请求 int connect(int sockfd, struct sockaddr* serv_addr, socklen_t addr_len);
//client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void ErrHandling(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];
int str_len;
if (argc != 3) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) {
ErrHandling("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
ErrHandling("connect() error");
}
str_len = read(sock, message, sizeof(message)-1);
if (str_len == -1) {
ErrHandling("read() error");
}
printf("Message from server : %s \n", message);
close(sock);
return 0;
}
运行结果如下
注意:由于没有开启端口复用,所以运行完成无法再次立即运行
1.2 基于linux的文件操作
linux中,socket也是文件的一种,所以可以使用文件IO的相关函数
0 | 标准输入:Standard Input |
1 | 标准输出:Standard Output |
2 | 标准错误:Standard Error |
打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/**
* @brief: open
* @param[1]:文件的地址
* @param[2]: 文件的打开模式
* @return:成功返回文件描述符,失败返回-1
*/
int open(const char* path, int flag)
打开模式 | 含义 |
---|---|
O_CREAT | 必要时创建文件 |
O_TRUNC | 删除全部现有数据 |
O_APPEND | 维持现有数据,保存到后面 |
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 读写 |
关闭文件
#include <unistd.h>
/**
* @brief:close
* @param:文件描述符
* @return:成功返回0,失败-1
*/
int close(int fd);
将数据写入文件
linux不区分文件与套接字,所以通过套接字向其他PC发送数据也用此函数
#include <unistd.h>
/**
* @param[1]:文件描述符
* @param[2]:要传输数据的地址
* @param[3]: 传输数据的字节数
* @return 是通过typedef声明的signed int
*/
ssize_t write(int fd, const void* buf, size_t nbytes)
读取数据
#include <unistd.h>
/**
*param[3]:要接受数据的最大字节数
*/
ssize read(int fd, void* buf, size_t nbytes);
- demo
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
const int kBufferSize = 100;
void ErrHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main() {
int fd;
char buf[] = "hello World!\n";
fd = open("./data.txt", O_RDWR | O_TRUNC);
if (fd == -1) {
ErrHandling("open() error");
}
printf("file descriptor: %d\n", fd);
printf("buf data: %s \n", buf);
if (write(fd, buf, sizeof(buf)) == -1) {
ErrHandling("write error");
}
close(fd);
fd = open("./data.txt", O_RDONLY);
if (fd == -1) {
ErrHandling("open() error");
}
char recv[kBufferSize];
if (read(fd, recv, sizeof(recv)) == -1) {
ErrHandling("read() error!");
}
printf("recieve data: %s\n", recv);
close(fd);
return 0;
}
不能写完立即读,因为文件指针写完在末尾,直接读啥也没有
1.3 基于Windows平台的操作
1.3.1 准备工作
要在 Windows 上进行套接字编程,需要:
- 链接 ws2_32.lib 库。在 VS 中通过:项目–>属性–>配置属性–>链接器–>输入–>附加依赖项 添加 ws2_32.lib 库即可。
- 导入头文件 WinSock2.h。Windows 中有一个 winsock.h 和一个 WinSock2.h。其中 WinSock2.h 是较新版本,用来代替前者的。
- 实际上在 windows 上还需要通过:项目–>属性–>配置属性–>C++ 将 SDL 检查设为否,否则运行会出错。
1.3.2 Winsock的初始化
进行 Winsock 编程时,必须首先调用 WSAStartup 函数,设置程序中用到的 Winsock 版本,并初始化相应版本的库。
#include <WinSock2.h>
// wVersionRequested:要用的 Winsock版本信息,lpWSAData:WSADATA 结构体变量的地址值
// 成功时返回 0,失败时返回非 0 的错误代码值
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
有必要给出两个参数的详细说明
- WORD wVersionRequested:WORD 类型是通过 typedef 定义的 unsigned short 类型。Winsock 中存在多个套接字版本,要选择需要的版本,0x0102 表示 1.2 版本。
可以用 MAKEWORD(2, 2) 来构造版本号,它构造了 2.2 版本的表示值,即返回 0x0202。 - LPWSADATA lpWSAData:LPWSADATA 是 WSADATA 类型的指针类型。没有特殊含义,只是为了调用函数,必须传递 WSADATA 类型变量的地址。
下面这段代码几乎是 Winsock 编程的公式。在进行 Winsock 编程时直接按下述方式编写即可。
int main(int argc, char* argv[])
{
WSADATA wsaData;
...
if(WSAStartup(MAKEWORD9(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
...
return 0;
}
1.3.3 注销
#include <WinSock2.h>
// 调用此函数,Winsock 相关库将还给操作系统,无法再调用 Winsock 相关函数。
// 成功时返回 0,失败时返回 SOCKET_ERROR
int WSACleanup(void);
1.4 Windows套接字相关函数和示例
#include <WinSock2.h>
// 成功时返回套接字句柄,失败时返回 INVALID_SOCKET
SOCKET socket(int af, int type, int protocol);
// 成功时返回 0,失败时返回 SOCKET_ERROR // 成功时返回 0,失败时返回 SOCKET_ERROR
int bind(SOCKET s, const struct sockaddr* name, int namelen);
// 成功时返回 0,失败时返回 SOCKET_ERROR
int listen(SOCKET s, int backlog);
// 成功时返回套接字句柄,失败时返回 INVALID_SOCKET
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
// 成功时返回 0,失败时返回 SOCKET_ERROR
int connect(SOCKET s, const struct sockaddr* name, int namelen);
// 成功时返回 0,失败时返回 SOCKET_ERROR
int closesocket(SOCKET s);
Windows 中的句柄相当于 Linux 中的文件描述符,但是 Windows 中要区分文件句柄和套接字句柄,两者不完全一样。
Linux 中套接字也是文件,因此可以通过文件 I/O 函数 read 和 write 来进行数据传输。而 Windows 中严格区分文件 I/O 函数和套接字 I/O 函数。
Winsock 数据传输函数包括下面两个:
#include <WinSock2.h>
/**
* s:套接字句柄;
* buf:待传输数据;
* len:要传输的字节数;
* flags:传输数据时用到的选项,一般可以写0。
*/
int send(SOCKET s, const char* buf, int len, int flags);
/**
* s:套接字句柄
* len:可接收的最大字节数
* flags:接收数据时用到的选项,一般可以写 0。
* 成功时返回接收的字节数(遇到文件尾 EOF 时返回 0),失败时返回 SOCKET_ERROR
*/
int recv(SOCKET s, const char* buf, int len, int flags);
demo
//hello_server.c
#pragma execution_character_set("utf-8")
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[] = "Hello World!";
if (argc != 2) // 检查参数数量
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化 Winsock 相关库
ErrorHandling("WSAStartup() error!");
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); // 设置 IP 地址
servAddr.sin_port = htons(atoi(argv[1])); // 设置端口号
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) // 为套接字分配地址和端口
ErrorHandling("bind() error");
if (listen(hServSock, 5) == SOCKET_ERROR) // 使套接字转换为可接收连接的状态
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr); // 接受连接请求,函数返回客户端的套接字
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
send(hClntSock, message, sizeof(message), 0); // 向客户端发送信息
closesocket(hClntSock); // 关闭服务器端套接字
closesocket(hServSock); // 关闭客户端套接字
WSACleanup(); // 注销 Winsock 相关库
return 0;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
//hello_cilent.c
#pragma execution_character_set("utf-8")
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strLen;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
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_un.S_addr = inet_addr(argv[1]); // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error!");
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;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}