一、理解网络编程和套接字
1.1 构建接电话套接字
套接字大致分为两种,其中TCP套接字可以比喻成电话机。首先讨论用于接听的套接字创建过程:
第一步是准备一个电话机。下列函数创建的就是相当于电话机的套接字:
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
//成功时返回文件描述符,失败时返回-1
准备好电话机后,我们需要考虑分配电话号码,这样别人才能联系到我们。套接字同样如此,就像给电话机分配电话号一样,利用以下函数可以给创建好的套接字分配地址信息(IP地址和端口号):
#include<sys/socket.h>
int bind(int sockfd,struct sockaddr *myaddr,socklen_t addrlen);
//成功时返回0,失败时返回-1
调用bind函数给套接字分配地址后,就基本完成了接电话的所有准备工作。接下来需要连接电话线并等待来电。
连接电话线后,电话机就转为可接听状态,这时其他人可以拨打电话请求连接到该机。同样,需要把套接字转化成可接收连接的状态:
#include<sys/socket.h>
int listen(int sockfd,int backlog);
//成功时返回0,失败时返回-1
连接好电话线后,如果有人拨打电话就会响铃,拿起话筒才能接听电话。拿起话筒意味着接受了对方的连接请求。套接字同样如此,如果有人为了完成数据传输而请求连接,就需要调用以下函数进行受理:
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
网络编程中接收连接请求的套接字创建过程可以整理如下:
- 调用socket函数创建套接字
- 调用bind函数分配IP地址和端口号
- 调用listen函数转为可接收请求状态
- 调用accept函数受理连接请求
1.2 编写”Hello World“服务器端
服务器端是能够受理连接请求的程序。下面构建服务器以验证之前提到的函数调用过程,该服务端收到的连接请求后向请求者返回”Hello World“答复:
//hello_server.c
#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);
}
//调用socket函数创建套接字
serv_sock=socket(PF_INET,SOCK_STREAM,0);
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]));
//调用bind函数分配IP地址和端口号
if(bind(serv_sock,(struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error");
//调用listen函数将套接字转化为可接收连接状态
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);
if(clnt_sock==-1)
error_handling("accept() error");
//调用accept函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock==-1)
error_handling("accept() error");
//用于传输数据
write(clnt_sock,message,sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
1.3 构建打电话套接字
服务端创建的套接字又称为服务器端套接字或监听套接字。接下来介绍用于请求连接的客户端套接字。用于请求连接的函数如下所示:
#include<sys/socket.h>
int connect(int sockfd,struct sockaddr *serv_addr,socklen_t addrlen);
//成功时返回0,失败时返回-1
客户端只有调用socket函数创建套接字和调用connect函数向服务端发送连接请求这两个步骤,因此比服务器端简单:
//hello_client.c
#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]));
//调用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);
}
二、基于Linux的文件操作
对Linux而言,socket操作与文件操作没有区别,因而需要详细了解文件。在Linux世界里,socket也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件I/O的相关函数。
2.1 文件描述符(File Descriptor)
如果想要使用Linux提供的文件I/O函数,首先应该理解好文件描述符的概念。这里的文件描述符是系统分配给文件或套接字的整数。实际上,学习C语言过程中用过的标准输入输出及标准错误在Linux中也被分配下表中的文件描述符:
文件描述符 | 对象 |
---|---|
0 | 标准输入:standard input |
1 | 标准输出:standard output |
2 | 标准错误:standard error |
文件和套接字一般经过创建过程才会被分配文件描述符,而上表中的三种输入输出对象即使未经特殊的创建过程,程序开始运行后也会被自动分配文件描述符。
每当生成文件或套接字时,操作系统将返回分配给它们的整数(即文件描述符)。这个整数将成为程序员与操作系统之间良好沟通的渠道。实际上,文件描述符只不过是为了方便称呼操作系统创建的文件或套接字而赋予的数而已。
文件描述符有时也称为文件句柄,但句柄主要是Windows平台的术语。
2.2 打开文件
打开文件的函数是open,调用此函数时需要传递两个参数:第一个参数是打开的目标文件名及路径信息,第二个参数是文件打开模式(文件特性信息)。
#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 | 读写打开 |
2.3 关闭文件
使用文件后必须关闭,关闭文件时调用的函数是close:
#include<unistd.h>
int close(int fd);
成功时返回0,失败时返回-1
- fd:需要关闭的文件或套接字的文件描述符
若调用此函数的同时传递文件描述符参数,则关闭(终止)相应文件。另外需要注意的是,此函数不仅可以关闭文件,还可以关闭套接字。
2.4 将数据写入文件
write函数用于向文件输出(传输)数据。由于Linux不区分文件与套接字,因此通过套接字向其他计算机传递数据时也会用到该函数。
#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。
2.5 读取文件中的数据
与之前的write函数相对应,read函数用来输入(接收)数据
#include<unistd.h>
ssize_t read(int fd,void *buf,size_t nbytes);
//成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
- fd:显示数据接收对象的文件描述符
- buf:要保存接收数据的缓冲地址值
- nbytes:要接收数据的最大字节数