TCP/IP网络编程笔记

《TCP/IP网络编程》学习笔记,如果感兴趣的小伙伴可以访问我的github仓库下载笔记和源码
github仓库:TCP/IP网络编程笔记

第1章 理解网络编程和套接字


1.1 理解网络编程和套接字

1.1.1 网络编程和套接字概要

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

套接字(socket):网络数据传输时用的软件设备。

网络编程又称套接字编程。

​ 电话机可以同时用来拨打或接听,但对套接字而言,拨打和接听是有区别的。我们先讨论用于接听的套接字创建过程。下面利用电话机讲解套接字的创建。

1.1.2 构建接电话套接字

  • 调用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.1.3 编写server套接字程序

服务器端(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;
	int clnt_sock;
	
	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);	// 调用socket函数创建套接字
	if(serv_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=htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr *) &serv_addr,  sizeof(serv_addr)) == -1)	//调用bind函数分配IP地址和端口号
		error_handling("bind() error");
	if(listen(serv_sock, 5) == -1)	// 调用listen函数将套接字转为可接收连接状态
	{
		error_handling("listen() error");
	}
	clnt_addr_size = sizeof(clnt_addr);
	clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);	// 调用accept函数受理连接请求。如果
																					// 在没有连接请求的情况下调用该函数,
																					// 则不会返回,直到有连接,直到有连接请求为止
	if(clnt_sock == -1)
		error_handling("accept() error");
	
	write(clnt_sock, message, sizeof(message));	// write函数用于传输数据,若程序经过第40行代码执行到本行,则说明已经有了连接请求。
	close(clnt_sock);
	close(serv_sock);
	return 0;
}

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

1.1.4 构建打电话套接字

  • 下面介绍用于请求连接的客户端套接字,客户端程序只有两个步骤:

    1. 调用socket函数创建套接字

    2. 调用connect函数向服务器端发送连接请求

  • connect函数:

      #include<sys/socket.h>
      int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addren);
    

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

1.1.5 编写client套接字程序

  • 客户端源码:
#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);		// 创建套接字,此时套接字还不能够分为服务器端和客户端。如果紧接着
												// 调用bind、listen函数,将成为服务器端套接字;如果调用connect函数
												//,将成为客户端套接字。
	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)	// 调用connect函数向服务器端发送连接请求
		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);
}

1.1.6 在Linux平台下运行

  1. hello_server.c示例进行编译的命令:

    gcc hello_server.c -o hserver
    ./hserver 9190			// 9190是服务器中开启的端口号
    

    编译hello_server.c文件并生成可执行文件hserver

  2. hello_client示例进行编译的命令:

    gcc hello_client.c -o hclient
    ./hclient 127.0.0.1 9190	// 127.0.0.1和9190是分别是服务器的ip地址和端口号
    

    此时,client将会接收到由服务器发来的字符串 Hello World!

1.2 基于Linux的文件操作

​ 对Linux而言,socket操作与文件操作没有区别,因而有必要详细了解文件。在Linux世界里,socket也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件I/O的相关函数。

1.2.1 底层文件访问和文件描述符

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

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

    文件和套接字一般经过创建过程才会被分配文件描述符。文件描述符有时也被称为文件句柄,但”句柄“主要是Windows中的术语。

1.2.2 打开文件

  • 首先介绍打开文件来读写数据的函数:

      #include<sys/types.h>
      #include<sys/stat.h>
      #include<fcnt1.h>
      
      int open(const char * path, int flag);	// 打开文件函数
    

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

    此函数需传递两个参数:

    • 第一个参数:path是打开的目标文件名及路径信息。
    • 第二个参数:flag是文件打开模式(文件特性信息)。
  • 文件打开模式:

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

1.2.3 关闭文件

  • 使用文件后必须关闭,下面介绍关闭文件时调用的函数:

      #include<unistd.h>
      int close(int fd);
    

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

    参数:fd,需要关闭的文件或套接字的文件描述符。

    此函数不仅可以关闭文件,还可以关闭套接字。这再次证明了“Linux操作系统不区分文件与套接字”的特点。

1.2.4 将数据写入文件

  • 接下来介绍的write函数用于向文件输出(传输)数据。Linux中不区分文件与套接字,因此通过套接字向其他计算机传递数据时也会用到该函数。前面示例 hello_server.c中也调用它传递字符串 "Hello World!"

      #include<unistd.h>
      ssize_t write(int fd, const void * buf, size_t nbytes);
    

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

    参数1:fd,显示数据传输对象的文件描述符。

    参数2:buf,保存要传输数据的缓冲地址值。

    参数3:nbytes,要传输数据的字节数。

    此函数定义中,size_t是通过typedef声明的unsigned int类型。对ssize_t来说,size_t前面多加的s代表signed,即ssize_t是通过typedef声明的signed int类型。

  • 创建新文件并保存数据:

    代码见:

    #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!

1.2.5 读取文件中的数据

  • 与之前的write函数相对应,read函数用来输入(接收)数据。

      #include<unistd.h>
      ssize_t read(int fd, void * buf, size_t nbytes);
    

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

    参数1:fd,显示数据接收对象的文件描述符。

    参数2:buf,要保存接收数据的缓冲地址值。

    参数3: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);
    }
    

    编译运行后输出结果为:

    file descriptor:3
    file data: Let's go!
    

    基于文件描述符的I/O操作相关介绍到此结束。该内容同样适用于套接字。

1.2.6 文件描述符与套接字

  • 下面源代码文件将同时创建文件和套接字,并用整数型态比较返回的文件描述符值。

    代码参见:

#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/socket.h>

int main(void)
{
    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;
}

运行结果:

file destriptor 1: 3
file descroptor 2: 4
file descriptor 3: 5

从输出的文件描述符整数值可以看出,描述符从3开始以由小到大的顺序编号,因为0、1、2是分配给标准I/O的描述符。


附上笔记源代码:https://github.com/Barry-xc/TCP-IP-SocketProgramming

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值