《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 构建打电话套接字
-
下面介绍用于请求连接的客户端套接字,客户端程序只有两个步骤:
-
调用socket函数创建套接字
-
调用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平台下运行
-
对
hello_server.c
示例进行编译的命令:gcc hello_server.c -o hserver ./hserver 9190 // 9190是服务器中开启的端口号
编译
hello_server.c
文件并生成可执行文件hserver
。 -
对
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