《TCP/IP网络编程》学习笔记 | Chapter 1:理解网络编程和套接字

《TCP/IP网络编程》学习笔记 | Chapter 1:理解网络编程和套接字

《TCP/IP网络编程》学习笔记 | Chapter 1:理解网络编程和套接字

基本概念

网络编程是什么?

编写程序使两台联网的计算机相互交换数据。

服务端

步骤 1:创建套接字

cpp 复制代码
int socket(int domain, int type, int protocol);

成功时返回文件描述符,失败时返回-1。

步骤 2:绑定IP地址和端口号

cpp 复制代码
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

成功时返回0,失败时返回-1

步骤 3:使转化为可接受请求状态

cpp 复制代码
int listen(int sockfd, int backlog);

成功时返回文件描述符,失败时返回-1。

步骤 4:受理请求连接

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

成功时返回文件描述符,失败时返回-1。

客户端

步骤 1:创建socket

步骤 2:发起连接请求

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

成功时返回0,失败返回-1。

基于 Linux 平台的 "Hello world!" 服务端和客户端

服务端程序:

cpp 复制代码
#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);
    }

    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]));

    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);
}

客户端程序:

cpp 复制代码
#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);
    }

    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]));

    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);
}

基于 Linux 的文件操作

Linux中一切皆文件,socket自然也是文件。

​ 每当生成文件或socket,操作系统都将返回分配给它们的整数(文件描述符),文件描述符只不过是为了方便的称呼操作系统创建的文件或socket而赋予的数。

​ 分配给标准输入,输出及标准错误的文件描述符。

文件描述符 对象
0 标准输入 :Standard Input
1 标准输出 :Standard Ouput
2 标准错误 :Standard Error

其他的文件描述符从3开始的。

打开文件

cpp 复制代码
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int fla);

成功时返回文件描述符,失败时返回-1。

参数:

  • path: 文件名的字符串地址

  • flag: 文件打开模式信息,如需传递多个参数,应通过 | 运算符组合。

打开模式 含义
O_CREAT 必要时创建文件
O_TRUNC 删除全部现有数据
O_APPEND 维持现有数据,保存到其后面
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开

关闭文件

cpp 复制代码
#include <unistd.h>

int close(int fildes);

成功时返回0,失败时返回-1。

参数:

  • fildes: 需要关闭的文件或socket的文件描述符

写文件

cpp 复制代码
#include <unistd.h>

ssize_t write(int fildes, const void *buf, size_t nbyte);

成功时返回写入的字节数,失败时返回-1。

参数:

  • fildes: 显示数据传输对象的文件描述符

  • buf:保存要传输数据的缓冲地址值

  • nbyte: 要传输数据的字节数

size_t : unsigned int,ssize_t:signed int,这些都是元数据类型,由操作系统定义,通过 typedef 声明。

示例程序:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(void)
{
    int fd;
    char buf[] = "Let's go!\n";

    fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC);
    if (fd == 1)
        error_handling("open() error!");
    printf("file descriptor: %d\n", fd);

    if (write(fd, buf, sizeof(buf)) == -1)
        error_handling("write() error!");
    close(fd);

    return 0;
}

运行结果:

注意:Windows 端可以用 type 命令打印 txt 文件的内容。

读文件

cpp 复制代码
#include <unistd.h>

ssize_t read(int fildes, void *buf, size_t nbyte);

成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1。

参数:

  • fildes:显示数据接收对象的文件描述符

  • buf:要保存接收数据的缓冲地址值

  • nbytes:要接收数据的最大字节数

示例程序:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#define BUF_SIZE 100

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(void)
{
    int fd;
    char buf[BUF_SIZE];

    fd = open("data.txt", O_RDONLY);
    if (fd == -1)
        error_handling("open() error!");

    printf("file descriptor: %d\n", fd);

    if (read(fd, buf, BUF_SIZE) == -1)
        error_handling("read() error!");

    printf("file data: %s", buf);

    close(fd);

    return 0;
}

运行结果:

基于 Windows 平台的实现

Windows 套接字,简称 Winsock,大部分参考 BSD 系列 UNIX 套接字设计,和 Linux 套接字类似。

Winsock 的初始化

进行Winsock编程时,首先调用 WSAStartup 函数,设置程序中用到的Winsock版本,并初始化相应版本的库。

cpp 复制代码
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);

参数:

  • wVersionRequested:Winsock版本信息
  • lpWSAData:WSADATA结构体变量的地址值

Winsock中存在多个版本,应准备WORD(typedef unsigned int WORD;)类型的套接字版本信息,若版本为1.2,则1是主版本号,2是副版本号,应传递 0x0201

​ 0x0201 高8位为副版本号,低8位为主版本号。这里借助 MAKEWORD 宏函数来传递版本信息号。

cpp 复制代码
MAKEWORD(1,2); // 主版本号是1,副版本号是2,返回0x0201
cpp 复制代码
int main(int argc,char* argv[])
{
	WSADATA wsaData;
	....
	if(WSAStartup(MAKEWORD(1,2), &wsaData) != 0)
	{
		....
	}
	return 0;
}

注销 Winsock 相关库

cpp 复制代码
#include <Winsock2.h>

int WSACleanup(void);

成功时返回0,失败时返回 SOCKET_ERROR。

基于 Windows 的套接字相关函数

创建socket:

cpp 复制代码
SOCKET socket(int af,int type,int protocol);

成功时返回套接字句柄(对应linux中的文件描述符),失败返回 INVALID_SOCKET。

绑定IP和端口号:

cpp 复制代码
int bind(SOCKET s,const struct sockaddr* name,int namelen);

成功返回0,失败返回SOCKET_ERROR。

将服务端设置为可接受请求状态:

cpp 复制代码
int listen(SOCKET s,int backlog);

成功返回0,失败返回SOCKET_ERROR。

受理请求连接(accept):

cpp 复制代码
SOCKET accept(SOCKET s,struct sockaddr* addr,int* addrlen);

成功时返回套接字句柄,失败时返回INVALID_SOCKET。

从客户端发起请求连接:

cpp 复制代码
int connect(SOCKET s,const struct sockaddr* name,int namelen);

成功时返回0,失败时返回SOCKET_ERROR。

关闭套接字:

cpp 复制代码
int closesocket(SOCKET s);

成功时返回0,失败时返回SOCKET_ERROR。

Windows中的句柄相当于linux中的文件描述符,但是Windows中的文件句柄和套接字句柄是有区别的。

创建基于 Windows 的服务端和客户端

服务端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHanding(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hServerSock, hClientSock;
	SOCKADDR_IN serverAddr, clientAddr;
	int szClientAddr;
	char message[] = "Hello World!";

	if (argc != 2)
	{
		printf("Usage: %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHanding("WSAStartup() 	error!");

	hServerSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServerSock == INVALID_SOCKET)
		ErrorHanding("socket() 	error!");

	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServerSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
		ErrorHanding("bind() error!");

	if (listen(hServerSock, 5) == SOCKET_ERROR)
		ErrorHanding("listen() error!");

	szClientAddr = sizeof(clientAddr);
	hClientSock = accept(hServerSock, (SOCKADDR *)&clientAddr, &szClientAddr);
	if (hClientSock == INVALID_SOCKET)
		ErrorHanding("accept() error!");

	send(hClientSock, message, sizeof(message), 0);

	closesocket(hClientSock);
	closesocket(hServerSock);

	WSACleanup();

	return 0;
}

客户端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHanding(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    SOCKADDR_IN serverAddr;

    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)
        ErrorHanding("WSAStartup() 	error!");

    hSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
        ErrorHanding("socket() 	error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        ErrorHanding("connect() error!");

    strLen = recv(hSocket, message, sizeof(message) - 1, 0);
    if (strLen == -1)
        ErrorHanding("recv() error!");

    printf("Message from server: %s\n", message);

    closesocket(hSocket);

    WSACleanup();

    return 0;
}

编译遇到报错:

参考 【三种解决方法】undefined reference to `__imp_WSAStartup' 给出的解决办法,手动添加编译参数 -lwsock32:

cpp 复制代码
gcc hello_server_win.c -lwsock32 -o hServerWin

gcc hello_client_win.c -lwsock32 -o hClientWin

编译就可以成功了:

我们运行服务端程序,监听 9190 端口:

hServerWin 9190

再运行客户端程序:

hClientWin 127.0.0.1 9190

得到输出:

cpp 复制代码
Message from server: Hello World!

基于 Windows 的 I/O 函数

Linux中socket也是文件,因此可以通过文件I/O函数read和write进行数据传输。而Windows严格区分文件I/O函数和套接字I/O函数。

cpp 复制代码
int send(SOCKET s,const char* buf,int len,int flags);

成功时返回传输字节数,失败时返回SOCKET_ERROR。

参数:

  • s:表示数据传输对象连接的套接字句柄值
  • buf:保存待传输数据的缓冲地址值
  • len:要传输的字节数
  • flags:传输数据时用到的多种选项信息
cpp 复制代码
int recv(SOCKET s, const char* buf, int len, int flags);

成功时返回接收的字节数(收到EOF时为0),失败返回SOCKET_ERROR。

  • s:表示数据接收对象连接的套接字句柄值
  • buf:保存待接收数据的缓冲地址值
  • len:要接收的字节数
  • flags:接收数据时用到的多种选项信息

习题

(1)套接字在网络编程中的作用是什么?为什么称它为套接字?

网络编程就是编写程序让两台联网的计算机相互交换数据。在我们不需要考虑物理连接的情况下,我们只需要考虑如何编写传输软件。操作系统提供了名为"套接字",套接字是网络传输传输用的软件设备。

socket英文原意是插座,我们把插头插到插座上就能从电网获得电力供给,同样,为了与远程计算机进行数据传输,需要连接到Internet,而变成中的"套接字"就是用来连接该网络的工具。

(2)在服务器端创建套接字后,会依次调用listen函数和accept函数。请比较并说明两者作用。

listen:将套接字转为可接受连接方式(监听套接字)

accept:受理连接请求,并且在没有连接请求的情况调用该函数,不会返回(阻塞)。直到有连接请求为止。二者存在逻辑上的先后关系。

(3)Linux中,对套接字数据进行I/O时可以直接使用I/O相关函数;而在Windows中则不可以。原因为何?

Linux把套接字也看作是文件,所以可以用文件I/O相关函数;而Windows要区分套接字和文件,所以设置了特殊的函数。

(4)创建套接字后一般会给它分配地址,为什么?为了完成地址分配需要调用哪些函数?

要在网络上区分来自不同机器的套接字,所以需要地址信息。分配地址是通过bind()函数实现。

(5)Linux中的文件描述符与Windows的句柄实际上非常类似。请以套接字为对象说明他们的含义。

Linux的文件描述符是为了区分指定文件而赋予文件的整数值(相当于编号)。Windows的文件描述符其实也是套接字的整数值,其目的也是区分指定套接字。

(6)底层文件I/O函数与ANSI标准定义的文件I/O函数之间有何区别?

  1. ANSI标准定义的输入、输出函数是与操作系统(内核)无关的以C标准写成的函数;相反,底层文件I/O函数是操作系统直接提供的。
  2. 标准I/O分为全缓冲,行缓冲,不缓冲三种形式;文件I/O为不带缓冲的I/O。
  3. 文件I/O主要针对文件操作,它操作的是文件描述符;标准I/O针对的是控制台,它操作的是字符流。对于不同设备得特性不一样,必须有不同API访问才最高效。
相关推荐
Jackilina_Stone22 分钟前
【Linux】网络相关的命令
网络·ubuntu·ip
MessiGo1 小时前
C/C++ 知识点:重载、覆盖和隐藏
c++
runing_an_min1 小时前
windows运行ffmpeg的脚本报错:av_ts2str、av_ts2timestr、av_err2str => E0029 C4576
c++·windows·ffmpeg·e0029
无敌岩雀1 小时前
C++设计模式结构型模式———桥接模式
c++·设计模式·桥接模式
麻由由哒哟2 小时前
CSP-J2023T4 旅游巴士(同余最短路)
c++·算法
攻城狮7号4 小时前
【5.5】指针算法-三指针解决颜色分类
c++·算法
OKkankan4 小时前
单链表的实现(数据结构)
java·c语言·数据结构·c++
gkdpjj4 小时前
二叉树中的深搜
c++·算法
醉颜凉4 小时前
【NOIP普及组】明明的随机数
java·c语言·数据结构·c++·算法
fhvyxyci5 小时前
【C++之STL】一文学会使用 string
开发语言·数据结构·c++·stl