本系列文章为《TCP/IP网络编程----尹圣雨》学习笔记
一、理解网络编程和套接字
概念
①网络编程: 编写程序使两台连网的计算机相互交换数据。
②套接字(socket): 在我们不需要考虑物理连接的情况下,我们只需要考虑如何编写传输软件。操作系统提供了名为套接字网络数据传输软件设备。因此,网络编程又称为套接字编程。
1.1 构建接电话套接字
示例说明
----套接字大致分为TCP套接字和UDP套接字两种。其中,TCP套接字可以比喻成电话机,电话机也是通过固定电话网完成语音数据交换的。因此,我们熟悉的固定电话与套接字实际并无太大区别。下面利用电话机讲解套接字的创建及使用方法。
----电话机可以同时用来拨打或接听,但对套接字而言,拨打和接听是有区别的。我们先讨论用于接听的套接字创建过程,下面利用电话机讲解套接字的创建。
----这里涉及的函数在后面章节会陆续说明,我们这里只是先拿来使用。
①调用socket函数(安装电话机)进行的对话
问:接电话需要准备什么?
答:当然是电话机啦!
有了电话机才能安装电话,接下来我们就准备一部电话机。下列函数创建的就是相当于电话机的套接字:
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
//成功时返回文件描述符,失败时返回-1。
准备好电话机后要考虑分配电话号码的问题,这样别人才能联系到自己。
②调用bind函数(分配电话号码)时进行的对话
问:请问您的电话号码是多少?
答:我的电话号是123-1234。
就像给电话机分配电话号码一样(虽然不是真的把电话号码给了电话机),利用以下函数给创建好的套接字分配地址信息(IP地址和端口号):
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addren);
//成功时返回0,失败时返回-1。
调用bind函数给套接字分配地址后,就基本完成了接电话的所有准备工作。接下来需要连接电话线并等待来电。
③调用listen函数(连接电话线)时进行的对话
问:已架设完电话机后是否只需连接电话线?
答:对,只需连接就能接听电话。
已连接电话线,电话机就转为可接听状态,这时其他人可以拨打电话请求连接到该机。同样,需要把套接字转化成可接收连接的状态:
#include<sys/socket.h>
int listen(int sockfd, int backlog);
//成功时返回0,失败时返回-1。
连接好电话线后,如果有人拨打电话就会响铃,拿起话筒才能接电话。
④调用accept函数(拿起话筒)时进行的对话
问:电话铃响了,我该怎么办?
答:接听啊!
拿起话筒意味着接受了对方的连接请求。套接字同样如此,如果有人为了完成数据传输而请求连接,就需要调用以下函数进行受理。
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//成功时返回文件描述符,失败时返回-1。
总结
网络编程中接受请求的套接字创建过程可整理如下:
①第一步:调用socket函数创建套接字。
②第二步:调用bind函数分配IP地址和端口号。
③第三步:调用listen函数转为可接受请求状态。
④第四步:调用accept函数受理连接请求。
1.2 编写"Hello world!"服务器端
服务器端(server)是能够受理连接请求的程序。下面构建服务器端以验证之前提到的函数调用过程,该服务器端收到连接请求后向请求者返回”Hello world!“答复。我们只关注套接字和相关函数的调用过程,不必理解全部示例,因为我们还未涉及任何实际编程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock; // server socket
int clnt_sock; // client socket
struct sockaddr_in serv_addr; // 主套接字,用于持续监听连接请求
struct sockaddr_in clnt_addr; // 辅助套接字:用于与客户端相连接以传输数据
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if (argc != 2) // 两个参数:可执行文件名、端口号
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0); // 创建了一个 TCP 套接字
if (serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr)); // 将 serv_addr 全部填充为 0,主要是为了将 serv_addr 的 sin_zero 成员设为 0
serv_addr.sin_family = AF_INET; // 选定 IPv4 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl:将 long 类型数据从主机字节序转换为网络字节序; INADDR_ANY:32 位整型值表示的 IP 地址
serv_addr.sin_port = htons(atoi(argv[1])); // 此程序运行时应该在文件名后跟一个端口号作为参数,如 hello_server 3030
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 将套接字与服务器的 IP 地址和端口号相绑定
error_handling("bind() error");
if (listen(serv_sock, 5) == -1) // 将套接字转换为接听状态
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); // 接受一个连接请求,并将 clnt_sock 套接字与其相连接
if (clnt_sock == -1)
error_handling("accept() error");
write(clnt_sock, message, sizeof(message)); // 向客户端发送信息。注意:clnt_sock 不是客户端的套接字,而是服务器上真正与客户端相连接的套接字
close(clnt_sock); // 关闭与客户连接的套接字:断开了该连接
close(serv_sock); // 关闭监听端口的套接字:不再接受任何请求
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
首先执行gcc hello_server.c -o hserver
编译源文件,然后执行./hserver 9190
等待客户端消息的来临。
1.3 构建打电话套接字
服务器端创建的套接字又称为服务器端套接字或监听( listening)套接字,用于请求连接的客户端套接字的创建过程简单得多。此外,客户端程序只有调用socket函数创建套接字
和调用connect函数向服务器端发送连接请求
这两个步骤,因此客户端比服务器端简单。
①调用socket函数创建套接字
问:“打电话需要准备什么?”
答:“当然是电话机啦!”
②调用connect函数向服务器端发送连接请求
#include<sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addren);
//成功时返回0,失败时返回-1。
发送连接请求后就等待回应即可。
1.4 编写"Hello world!"客户端
客户端只需要完成两项内容:①调用socket函数和connect函数②与服务器端共同运行以收发字符串数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
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 <IP> <port>\n", argv[0]);
exit(1);
}
//创建套接字,此时套接字并不马上分为服务端和客户端。如果紧接着调用 bind,listen 函数,将成为服务器套接字
//如果调用 connect 函数,将成为客户端套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("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]));
//调用 connect 函数向服务器发送连接请求
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
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;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
首先执行gcc hello_client.c -o hclient
编译源文件,然后执行./hclient 192.168.123.128 9190
向服务器端发送消息,最后收到服务器发回的消息:Hello World!
。
二、基于Linux的文件操作
背景
对Linux而言,socket操作与文件操作没有区别,因而有必要详细了解文件。在Linux世界里,socket也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件I/O的相关函数。Windows与Linux不同,是要区分socket和文件的。因此在Windows中需要调用特殊的数据传输相关函数。
文件描述符
定义: 系统分配给文件或套接字的整数。
理解文件描述符:
----学校附近有个服务站,只需打个电话就能复印所需论文。
----服务站有位常客叫英秀,他每次都要求复印同一篇论文的一部分内容。比如《关于随着高度信息化社会而逐步提升地位的触觉、知觉、思维、性格、智力等人类生活质量相关问题特性的人类学研究》这篇论文第26页到第30页。
----这位同学每天这样打好几次电话,并且语速还特别慢,特别浪费时间。
----于是服务站的大叔将这篇论文编为18号,以后秀英可以直接说帮我复印18号论文26页到30页。
----之后,大叔会给另外一些标题很长的论文分配不重复的编号,这样效率提高了许多。
角色:
①大叔:操作系统
②英秀:程序员
③论文编号:文件描述符
④论文:文件或套接字。
总结: 也就是说,每当生成文件或套接字,操作系统将返回分配给它们的整数。这个整数将成为程序员与操作系统之间良好沟通的渠道。实际上,文件描述符只不过是为了方便称呼操作系统创建的文件或套接字而赋予的数而已。
此外,学习C语言过程中用过的标准输入输出及标准错误在Linux中也被分配为以下文件描述符。
文件描述符 | 对象 |
---|---|
0 | 标准输入:Standard Input |
1 | 标准输出:Standard Output |
2 | 标准错误:Standard Error |
文件和套接字一般经过创建过程才会被分配文件描述符。文件描述符有时也被称为文件句柄,但句柄主要是Windows中的术语。
2.1 将数据写入文件
打开文件
首先介绍打开文件以读写数据的函数。调用此函数时需传递两个参数:第一个参数是打开的目标文件名及路径信息,第二个参数是文件打开模式(文件特性信息)。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);
/*
成功时返回文件描述符,失败时返回-1
path : 文件名的字符串地址
flag : 文件打开模式信息
*/
下表是此函数第二个参数flag可能的常量值及含义。如需传递多个参数,则应通过位或运算(OR)符组合并传递。
打开模式 | 含义 |
---|---|
O_CREAT | 必要时创建文件 |
O_TRUNC | 删除全部现有数据 |
O_APPEND | 维持现有数据,保存到其后面 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
关闭文件
使用文件后必须关闭,下面介绍关闭文件时调用的函数:
#include<unistd.h>
int close(int fd);
/*
成功时返回0,失败时返回-1
参数:fd,需要关闭的文件或套接字的文件描述符。
*/
此函数不仅可以关闭文件,还可以关闭套接字。这再次证明了Linux操作系统不区分文件与套接字的特点。
将数据写入文件
前面介绍的write函数用于向文件输出(传输)数据。当然,Linux中不区分文件与套接字。因此,通过套接字向其他计算机传递数据时也会用到该函数。之前的示例也调用它传递字符串“Hello World!”。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
/*
成功时返回写入的字节数 ,失败时返回 -1
fd : 显示数据传输对象的文件描述符
buf : 保存要传输数据的缓冲值地址
nbytes : 要传输数据的字节数
*/
此函数定义中,size_t是通过typedef声明的unsigned int类型。对ssize_t来说,size_t前面多加的s代表signed,即ssize_t是通过typedef声明的signed int类型。
以_t为后缀的数据类型
问题: 我们已经接触到
ssize_t
、size_t
等陌生的数据类型。这些都是元数据类型(primitive),在sys/types.h头文件中一般由typedef声明定义,算是给大家熟悉的基本数据类型起了别名。既然已经有了基本数据类型,为何还要声明并使用这些新的呢?
答案: 人们目前普遍认为int是32位的,因为主流操作系统和计算机仍采用32位。而在过去16位操作系统时代,int类型是16位的。根据系统的不同、时代的变化,数据类型的表现形式也随之改变,需要修改程序中使用的数据类型。如果之前已在需要声明4字节数据类型之处使用了size_t或ssize_t,则将大大减少代码变动,因为只需要修改并编译size_t和ssize_t的typedef声明即可。在项目中,为了给基本数据类型赋予别名,一般会添加大量typedef声明。而为了与程序员定义的新数据类型加以区分,操作系统定义的数据类型会添加后缀_t。
示例: 创建新文件data.txt并保存数据
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
void error_handling(char * message);
int main(void)
{
int fd;
char buf[] = "Let's go!\n";
fd = open("data.txt", O_CREAT| O_WRONLY| O_TRUNC); //文件打开模式为O_CREAT、 O_WRONLY、 O_TRUNC的组合,因此
//将创建空文件,并只能写。若存在data.txt文件,则清空文件的全部数据。
if(fd == -1)
error_handling("open() error!");
printf("file descriptor: %d \n", fd);
if(write(fd, buf, sizeof(buf)) == -1) // 相对应于fd中保存的文件描述符的文件传输buf中保存的数据。
error_handling("write() error!");
close(fd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
执行gcc low_open.c -o lopen
编译源文件,执行./lopen
运行程序。可以看到新增了一个data.txt文件,里面的内容正是我们在代码中写入的Let’s go!
2.2 读取文件中的数据
读取文件中的数据
与之前的write函数相对应,read函数用来输入(接收)数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
/*
成功时返回接收的字节数(但遇到文件结尾则返回 0),失败时返回 -1
fd : 显示数据接收对象的文件描述符
buf : 要保存接收的数据的缓冲地址值。
nbytes : 要接收数据的最大字节数
*/
示例: 通过read函数读取data.txt文件中的内容
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#define BUF_SIZE 100
void error_handling(char * message);
int main(void)
{
int fd;
char buf[BUF_SIZE];
fd = open("data.txt", O_RDONLY); // 打开读取专用文件data.txt
if(fd == -1)
{
error_handling("open() error!");
}
printf("file descriptor: %d \n", fd);
if(read(fd, buf, sizeof(buf)) == -1) // 调用read函数向第11行中声明的数组buf保存读入的数据。
error_handling("read() error!");
printf("file data: %s", buf);
close(fd);
return 0;
}
void error_handling(char * message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
执行gcc low_read.c -o lread
编译源文件,执行./lread
运行程序。可以看到将data.txt中的内容读取出来了。
2.3 文件描述符与套接字
下面将同时创建文件和套接字,并用整数型态比较返回的文件描述符值。
nclude <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main()
{
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);
printf("file descriptor 2: %d\n", fd2);
printf("file descriptor 3: %d\n", fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
执行gcc fd_seri.c -o fds
编译源文件,执行./fds
运行程序。从输出的文件描述符整数值可以看出,描述符从3开始以由小到大的顺序编号(numbering ),因为0、1、2是分配给标准IO的描述符。
三、基于Windows平台的实现
同时学习Linux和 Windows的原因
①Windows套接字(以下简称Winsock)大部分是参考BSD系列UNIX套接字设计的,所以很多地方都跟Linux套接字类似。因此,只需要更改Linux环境下编好的一部分网络程序内容,就能在Windows平台下运行。
②大多数项目都在Linux系列的操作系统下开发服务器端,而多数客户端是在Windows平台下开发的。不仅如此,有时应用程序还需要在两个平台之间相互切换。因此,学习套接字编程的过程中,有必要兼顾Windows和Linux两大平台。另外,这两大平台下的套接字编程非常类似,如果把其中相似的部分放在一起讲解,将大大提高学习效率。
3.1 为Windows套接字编程设置头文件和库
要在Windows上进行套接字编程,需要:
①链接ws2_32.lib库。在VS中通过:项目->属性->配置属性->链接器->输入->附加依赖项 添加 ws2_32.lib库即可。
②导入头文件WinSock2.h。Windows中有一个winsock.h和一个WinSock2.h。其中WinSock2.h 是较新版本,用来代替前者的。实际上在windows上还需要通过:项目->属性->配置属性->C++ 将 SDL检查设为否,否则运行会出错。
3.2 Winsock的初始化
进行Winsock编程时,必须首先调用WSAStartup函数,设置程序中用到的Winsock版本,并初始化相应版本的库。
#include <WinSock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); // wVersionRequested:要用的 Winsock版本信息,lpWSAData:WSADATA 结构体变量的地址值
// 成功时返回 0,失败时返回非 0 的错误代码值
参数说明
①WORD wVersionRequested
----Winsock中存在多个版本,应准备WORD类型的(WORD是通过typedef声明定义的unsigned short类型)套接字版本信息,并传递给该函数的第一个参数wVersionRequested。若版本为1.2,则其中1是主版本号,2是副版本号,应传递0x0201。
----如前所述,高8位为副版本号,低8位为主版本号,以此进行传递。本书主要使用2.2版本,故应传递0x0202。不过,以字节为单位手动构造版本信息有些麻烦,借助MAKEWORD宏函数则能轻松构建WORD型版本信息。
----MAKEWORD(1,2);
:主版本为1,副版本为2,返回0x0201。
----MAKEWORD(2,2);
:主版本为2,副版本为2,返回0x0202。
②LPWSADATA lpWSAData
此参数中需传入WSADATA型结构体变量地址(LPWSADATA是WSADATA的指针类型)。调用完函数后,相应参数中将填充已初始化的库信息。虽无特殊含义,但为了调用函数,必须传递WSADATA结构体变量地址。
下面给出WSAStartup函数调用过程,这段代码几乎已成为Winsock编程的公式。
int main(int argc, char* argv[])
{
WSADATA wsaData;
...
if(WSAStartup(MAKEWORD9(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
...
return 0;
}
前面已经介绍了Winsock相关库的初始化方法,接下来讲解如何注销该库----利用下面给出的函数。
#include <WinSock2.h>
int WSACleanup(void); // 调用此函数,Winsock 相关库将还给操作系统,无法再调用 Winsock 相关函数。
// 成功时返回 0,失败时返回 SOCKET_ERROR
四、基于Windows的套接字相关函数及示例
4.1 基于Windows的套接字相关函数
下列是基于Windows的套接字相关函数,虽然返回值和参数与Linux函数有所区别,但具有相同功能的函数名是一样的。正是这些特点使跨越两大操作系统平台的网络编程更加简单。
#include <WinSock2.h>
SOCKET socket(int af, int type, int protocol); // 成功时返回套接字句柄,失败时返回 INVALID_SOCKET
//与Linux的bind函数相同,调用其分配IP地址和端口号。
int bind(SOCKET s, const struct sockaddr* name, int namelen); // 成功时返回 0,失败时返回 SOCKET_ERROR
//与Linux的listen函数相同,调用其使套接字可接收客户端连接。
int listen(SOCKET s, int backlog); // 成功时返回 0,失败时返回 SOCKET_ERROR
//与Linux的accept函数相同,调用其受理客户端连接请求。
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen); // 成功时返回套接字句柄,失败时返回 INVALID_SOCKET
//与Linux的connect函数相同,调用其从客户端发送连接请求。
int connect(SOCKET s, const struct sockaddr* name, int namelen); // 成功时返回 0,失败时返回 SOCKET_ERROR
//这个函数在关闭套接字时调用。Linux中,关闭文件和套接字时都会调用close函数;而Windows中有专门用来关闭套接字的函数。
int closesocket(SOCKET s); // 成功时返回 0,失败时返回 SOCKET_ERROR
4.2 Windows中的文件句柄和套接字句柄
Windows与Linux的套接字
----Linux内部也将套接字当作文件。因此,不管创建文件还是套接字都返回文件描述符。之前也通过示例介绍了文件描述符返回及编号的过程。
----Windows中通过调用系统函数创建文件时,返回句柄(handle)。换言之,Windows中的句柄相当于Linux中的文件描述符。只不过Windows中要区分文件句柄和套接字句柄。虽然都称为句柄,但不像Linux那样完全一致。文件句柄相关函数与套接字句柄相关函数是有区别的,这一点不同于Linux文件描述符。
Windows的套接字句柄
观察基于Windows的套接字相关函数,这将加深各位对SOCKET类型的参数和返回值的理解。这就是为了保存套接字句柄整型值的新数据类型,它由typedef声明定义。回顾socket、listen和accept等套接字相关函数,则更能体会到与Linux中套接字相关函数的相似性。
造成差异的原因
有些程序员可能会问:既然Winsock是以UNIX、Linux系列的BSD套接字为原型设计的,为什么不照搬过来,而是存在一定差异呢?
----有人认为这是微软为了防止UNIX、Linux服务器端直接移植到Windows而故意为之。
----从网络程序移植性角度上看,这也是可以理解的。
----但我有不同意见。从本质上说,两种操作系统内核结构上存在巨大差异,而依赖于操作系统的代码实现风格也不尽相同,连Windows程序员给变量命名的方式也不同于Linux程序员。从各方面考虑,保持这种差异性就显得比较自然。因此我个人认为,Windows套接字与BSD系列的套接字编程方式有所不同是为了保持这种自然差异性。
4.3 创建基于Windows的服务器端和客户端
Windows服务器端代码
#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);
}
打开cmd,执行hServerWin 9190
等待客户端的消息。
Windows客户端代码
#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);
}
打开另外一个cmd,执行hClientWin 192.168.31.222 9190
连接到服务器端。
4.4 基于Windows的I/O函数
Linux中套接字也是文件,因此可以通过文件I/O函数read和write来进行数据传输。而Windows中严格区分文件I/O函数和套接字I/O函数。
Winsock数据传输函数包括下面两个:
#include <WinSock2.h>
int send(SOCKET s, const char* buf, int len, int flags);
/*
成功时返回传输字节数,失败时返回SOCKET_ERROR
s:数据传输对象连接的套接字句柄值
buf:保存待传输数据的缓冲地址值
len:要传输的字节数
flags:传输数据时用到的多种选项信息,一般可以写0。
*/
int recv(SOCKET s, const char* buf, int len, int flags);
/*
成功时返回接收的字节数(收到文件尾 EOF 时为 0),失败时返回 SOCKET_ERROR
s:数据接受对象连接的套接字句柄值
buf:保存接受数据的缓冲地址值
len:能够接收的最大字节数
flags:接收数据时用到的多种选项信息
*/
注:
Windows的send函数与Linux的write函数相比,只是多出了最后的flags参数。后续章节中将给出该参数的详细说明,在此之前只需传递0,表示不设置任何选项。但有一点需要注意,send函数并非Windows独有。Linux中也有同样的函数,它也来自于BSD套接字。只不过我们在Linux相关示例中暂时只使用read、write函数,为了强调Linux环境下文件IO和套接字IO相同。