linux程序设计(套接字)+TCP/IP网络编程学习笔记

时间:2021-07-17 02:00:31

linux程序设计(套接字)+TCP/IP网络编程学习笔记


什么是套接字?

应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口,区分不同应用程序进程间的网络通信和连接。

网络化的应用程序在开始任何通讯之前都必需要创建套接字。就像电话的插口一样,没有它就完全没办法通信。

生成套接字,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。

Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中.

linux程序设计(套接字)+TCP/IP网络编程学习笔记

Host A上的程序A将一段信息写入Socket中,Socket的内容被Host A的网络管理软件访问,并将这段信息通过Host A的网络接口卡发送到Host B,Host B的网络接口卡接收到这段信息后,传送给Host B的网络管理软件,网络管理软件将这段信息保存在Host B的Socket中,然后程序B才能在Socket中阅读这段信息。

通过套接字接口可以实现网络间的进程通信.

端口号就是在同一操作系统内为区分不同套接字而设置的,因此无法将1个端口号分配给不同的套接字.虽然端口号不能重复,但TCP套接字和UDP套接字不会公用端口号,所以允许重复.

套接字是一种通信机制,这使得客户/服务器系统的开发工作即可以在本地单机上进行,也可以跨网络进行.linux所提供的功能(如打印服务,连接数据库和提供web页面)和网络工具(如用于远程登录的rlogin和用于文件传输的ftp)通常都是通过套接字来进行通信的.
套接字明确的将客户和服务器区分开来,这与管道是有区别的.套接字机制可以实现将多个客户连接到一个服务器.

基于Linux的文件操作

对于Linux而言,socket操作与文件操作没有区别,socket被认为是文件的一种,因此在网络数据传输的过程中可以使用文件I/O的相关函数.

文件描述符是系统分配给文件或套接字的整数

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

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

1.打开文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path,int flag);
成功时返回文件描述符,失败返回-1
  • path 文件名的字符串地址
  • flag 文件打开模式信息(如需传递多个参数,则应通过位或运算(OR)符组合并传递)

文件打开模式:

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

2.关闭文件

#include <unistd.h>
int close(int fd);
成功时返回0,失败返回-1
  • fd 需要关闭的文件或套接字的文件描述符

3. 将数据写入文件

write函数用于向文件输出(传输)数据.Linux系统中不区分文件与套接字,通过套接字向其他计算机传递数据时也用write函数

#include <unistd.h>
ssize_t wirte(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类型

4.读取文件中的数据

#include <unistd.h>
ssize_t read(int fd,void *buf,size_t nbytes);
成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
  • fd 显示数据接收对象的文件描述符
  • buf 要保存接收数据的缓冲地址值
  • nbytes 要接收数据的最大字节数

文件描述符与套接字

#include <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_STREAM,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;
}

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

套接字连接

理解套接字应用程序如何通过套接字维持一个连接?
1. 服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他进程共享.
2. 服务器进程给创建的套接字分配一个名字.本地套接字的名字是linux文件系统中的文件名,一般放在/tmp或/usr/tmp目录中.网络套接字的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点).该标识符允许linux将进入的针对特定端口号的连接转到正确的服务器进程.

  • 系统调用bind给套接字命名,然后服务器进程就开始等待客户连接到这个命名套接字.
  • 系统调用listen创建一个用于存放来自客户的进入连接的队列
  • 服务器通过系统调用accept来接受客户的连接

3 服务器调用accept时,会创建一个与原有的命名套接字不同的新套接字.新建套接字只用于与这个特定的客户进行通信,而命名套接字被保留下来继续处理来自其他客户的连接.服务器可以同时接受多个连接.对于一个简单的服务器,后续的客户将在监听队列中等待,直到服务器再次准备就绪.
4. 基于套接字系统的客户端更加简单.客户首先调用socket创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接.
5. 一旦连接建立,就可以像使用底层文件描述符那样用套接字来实现双向的数据通信.

套接字属性

3个属性决定套接字的特性:域(domain),类型(type),以及协议(protocol).套接字还用地址作为它的名字.地址的格式随域(又被称为协议族,protocol family)的不同而不同.每个协议族又可以使用一个或多个地址族来定义地址格式.(每种地址族适用的地址族均不同)

1. 套接字的域(域又称为协议族)

域指定套接字通信中使用的网络介质.最常见的套接字域是AF_INET,它指的是Internet网络.其底层的协议–网际协议(IP)只有一个地址族,它使用IP地址来指定网络中的计算机.

客户通过IP端口指定一台联网机器上的某个特定的服务.系统内部,使用一个唯一的16位整数来标识端口;系统外部,需要通过IP地址和端口号的组合来确定.套接字作为通信的终点,它必须在开始通信之前绑定一个端口.

服务器在特定的端口等待客户的连接.标准服务对应知名端口号(标准端口号).本地服务可以使用非标准的端口地址.

UNIX文件系统域AF_UNIX,一台位联网的计算机上的套接字也可以使用该域.这个域的底层协议就是文件输入/输出,而它的地址就是文件名.

头文件sys/socket.h中声明的协议族

名称 协议族
PF_INET IPv4互联网协议族
PF_INET6 IPv6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell协议族

套接字中实际采用的最终协议信息是通过socket函数的第三个参数传递的.在指定的协议族范围内通过第一个参数决定第三个参数.

2. 套接字类型

套接字类型指的是套接字的数据传输方式,该类型决定了创建的套接字的数据传输方式.
一个套接字域可能有多种不同的通信方式,而每种通信方式又有其不同的特性.但AF_UNIX域的套接字提供了一个可靠的双向通信路径.

因特网协议提供了两种通信机制:流(stream)和数据包(datagram).

面向连接的套接字(SOCK_STREAM)

流套接字(在某些方面类似与标准的输入/输出流)提供的是一个有序,可靠,双向字节流的连接.因此发送的数据可以确保不会丢失,复制或乱序到达,并且在这一过程中发生的错误也不会显示出来.大的消息将被分片,传输,再重组.这很像一个文件流,它接收大量的数据,然后以小数据块的形式将它们写入底层磁盘.流套接字的行为是可预见的.

由类型SOCK_STREAM指定流套接字,它们是在AF_INET域中通过TCP/IP连接实现的.它们也是AF_UNIX域中常用的套接字类型.

TCP/IP代表的是传输控制协议(Transmission Control Protocol)/网际协议(Internet Protocol).IP协议是针对数据包的底层协议,它提供从一台计算机通过网络到达另一台计算机的路由.TCP协议提供排序,流控和重传,以确保大数据的传输可以完整的到达目的地或报告一个适当的错误条件.

SOCK_STREAM特征:

  • 传输过程中数据不会消失
  • 按序传输数据
  • 传输的数据不存在数据边界

收发数据的套接字内部有缓冲(buffer),即字节数组.通过套接字传输的数据将保存到该数组.因此,收数据并不意味着马上调用read函数,只要不超过数组容量,则有可能在数据填充满缓冲后通过1次read函数调用读取全部,也有可能分成多次read函数调用进行读取.

面向消息的套接字(SOCK_DGRAM)

由类型SOCK_DGRAM指定的数据报套接字不建立和维持一个连接.它对可以发送的数据报的长度有限制.数据报作为一个单独的网络消息被传输,它可能会丢失,复制或乱序到达.

数据报套接字是在AF_INET域中通过UDP/IP连接实现的,它提供的是一种无序的不可靠服务.优点:开销较小,速度快.

数据报适用于信息服务中的”单次”(single-shot)查询,它主要用来提供日常状态信息或执行低优先级的日志记录.它的优点是服务器的崩溃不会给客户端造成不便,也不会要求客户重启,因为基于数据报的服务器通常不保留连接信息,所以它们可以在不打扰其客户的前提下停止并重启.

SOCK_DGRAM特征:

  • 强调快速传输而非传输顺序
  • 传输的数据可能丢失也可能损毁
  • 传输的数据有数据边界
  • 限制每次传输的数据的大小
  • 存在数据边界(意味着接收数据的次数应和传输次数相同)

套接字协议

只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议.(UNIX网络套接字和文件系统套接字)
socket函数的前两个参数传递了协议族信息和套接字数据传输方式,大部分情况下可以向第三个参数传递0,除非遇到下面的情况:

同一协议族中存在多个数据传输方式相同的协议
数据传输方式相同,但协议不同,此时需要通过第三个参数具体指定协议信息.

  • IPPROTO_TCP “IPv4协议族中面向连接的套接字”
  • IPPROTO_UDP “IPv4协议族中面向消息的套接字”

TCP服务器端的默认函数调用顺序

  • socket()创建套接字
  • bind()分配套接字地址
  • listen()等待连接请求状态
  • accept()允许连接
  • read()/write()数据交换
  • close()断开连接
    linux程序设计(套接字)+TCP/IP网络编程学习笔记

TCP客户端的默认函数调用顺序

  • socket()创建套接字
  • connect()请求连接
  • read()/write()交换数据
  • close()断开连接
    linux程序设计(套接字)+TCP/IP网络编程学习笔记

基于TCP/IP的服务器端/客户端函数调用关系

linux程序设计(套接字)+TCP/IP网络编程学习笔记
整体流程如下:服务器端创建套接字后连续调用bind,listen函数进入等待状态,客户端通过调用connect函数发起连接请求.客户端只能等到服务器端调用listen函数后才能调connect函数.客户端调用connect函数前,服务器端可能先调用accept函数,此时服务器端在调用accept函数时进入阻塞状态,直到客户端调用connect函数为止.

创建套接字

socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字.

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain,int type,int protocol);
成功时返回文件描述符,失败时返回-1

创建的套接字是一条通信线路的一个端点.

  • domain指定协议族(套接字中使用的协议族信息)
  • type参数指定这个套接字的通信类型(套接字数据传输类型)
  • protocol参数指定使用的协议(计算机间通信中使用的协议信息)

每种协议族适用的地址族均不同,如IPv4使用4字节地址族,IPv6使用16字节地址族.

关于PF_INET和AF_INET的区别?

在写网络程序的时候,建立TCP socket:

 sock = socket(PF_INET, SOCK_STREAM, 0);

然后在绑定本地地址或连接远程地址时需要初始化sockaddr_in结构,其中指定address family时一般设置为AF_INET,即使用IP。
相关头文件中的定义:

AF = Address Family
PF = Protocol Family
AF_INET = PF_INET

在windows中的Winsock2.h中,

#define AF_INET 0
#define PF_INET AF_INET

所以在windows中AF_INET与PF_INET完全一样.

而在Unix/Linux系统中,在不同的版本中这两者有微小差别.对于BSD,是AF,对于POSIX是PF.

建议:对于socket的domain参数,使用PF_LOCAL系列,
而在初始化套接口地址结构时,则使用AF_LOCAL.

常用地址族:

说明
AF_UNIX UNIX域协议(文件系统套接字)
AF_INET ARPR因特网协议(UNIX网络套接字)(/td)
AF_ISO ISO标准协议
AF_NS 施乐(Xerox)网络系统协议
AF_IPX Novell IPX协议
AF_APPLETALK Appletalk DDS

最常用的套接字域是AF_UNIX和AF_INET,AF_UNIX用于通过UNIX和Linux文件系统实现的本地套接字,AF_INET用于UNIX网络套接字.AF_INET套接字可以用于通过包括因特网在内的TCP/IP网络进行通信的程序.微软Windows系统的Winsock接口也提供了对这个套接字域的访问功能.

type参数指定用于新套接字的通信特性.它的取值包括SOCK_STREAM和SOCK_DGRAM.

  • SOCK_STREAM是一个有序,可靠,面向连接的双向字节流.对于AF_INET域的套接字,它默认是通过一个TCP连接来提供这一特性,TCP连接在两个流套接字端点之间建立.数据可以通过套接字连接进行双向传递.TCP协议所提供的机制可以用于分片和重组长消息,并且可以重传可能在网络中丢失的数据.
  • SOCK_DGRAM是数据报服务.可以用它发送最大长度固定(通常比较小)的消息,但消息是否会被正确传递或消息是否不会乱序到达并没有保证.对于AF_INET域套接字来说,这种类型的通信是由UDP数据报来提供的.

一般由套接字类型和套接字域决定通信所用协议,通常将protocol设置为0来表示使用默认协议.

socket系统调用返回一个描述符,它在许多方面都类似于底层的文件描述符.当连接到另一端的套接字后,就可以用read和write系统调用了,通过这个描述符来在套接字上发送和接收数据.close系统调用用于结束套接字连接.

套接字地址

POSIX是为UNIX系列操作系统设立的标准,它定义了一些数据类型,如表:

数据类型名称 数据类型说明 声明的头文件
int8_t singed 8-bit int sys/types.h
uint8_t unsinged 8-bit int(unsigned char) sys/types.h
int16_t singed 16-bit int sys/types.h
uint16_t unsinged 16-bit int(unsigned short) sys/types.h
int32_t singed 32-bit int sys/types.h
uint32_t unsinged 32-bit int(unsigned long) sys/types.h
sa_family_t 地址族(address family) sys/socket.h
socklen_t 长度(length of struct) sys/socket.h
in_addr_t IP地址,声明为uint32_t netinet/in.h
in_port_t 端口号,声明为uint16_t netinet/in.h

每个套接字域都有其地址格式.对于AF_UNIX域套接字,它的地址由结构sockaddr_un描述,该结构定义在头文件sys/un.h中(文件系统套接字)

struct sockaddr_un{
sa_family_t sun_family; /*AF_UNIX*/
char sun_path[]; /*pathname*/
};

对套接字进行处理的系统调用也许会接受不同类型的地址,每种地址格式都使用一种类似的结构描述,它们都以一个指定地址类型(套接字域)的成员sun_family开始.在AF_UNIX域中,套接字地址由结构中的sun_path成员中的文件名指定.

在当前的Linux系统中,由X/Open规范定义的类型sa_family_t在头文件sys/un.h中声明,它是短整数类型.sun_path指定的路径名长度是有限制的(Linux规定的是108个字符,其他系统可能使用的是更清楚的常量,如UNIX_MAX_PATH).因为地址结构的长度不一致,所以许多套接字调用需要用到一个用来复制特定地址结构的长度变量或将它作为一个输出.

//server
//创建一个服务器套接字,将它绑定到一个名字,然后创建一个监听队列,开始接受客户的连接.

//包含必要的头文件并设置变量:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
int server_sockfd,client_sockfd;
int server_len,client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;

//删除以前的套接字,为服务器创建一个未命名的套接字
unlink("server_socket");
server_sockfd=socket(AF_UNIX,SOCK_STREAM,0);

//命令套接字
server_address.sun_family=AF_UNIX;
strcpy(server_address.sun_path,"server_socket");
server_len=sizeof(server_address);
bind(server_sockfd,(struct sockaddr *)&server_address,server_len);

//创建一个连接队列,开始等待客户进行连接
listen(server_sockfd,5);
while(1)
{
char ch;
printf("server waiting\n");

//接受一个连接:
client_len=sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);
//对client_sockfd套接字上的客户进行读写操作
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}

}
//client
//包含一些必要的头文件并设置变量
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch='A';

//为客户创建一个套接字
sockfd=socket(AF_UNIX,SOCK_STREAM,0);

//根据服务器的情况给套接字命令
address.sun_family=AF_UNIX;
strcpy(address.sun_path,"server_socket");
len=sizeof(address);

//将我们的套接字连接到服务器的套接字:
result=connect(sockfd,(struct sockaddr *)&address,len);
if(result == -1)
{
perror("oops:client1");
exit(1);
}

//现在就可以通过sockfd进行读写操作了
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c\n",ch);
close(sockfd);
close(sockfd);
exit(0);
}

在AF_INET域中,套接字地址由定义在netinet/in.h中的结构sockaddr_in来指定:(网络套接字),此结构体作为地址信息传递给bind函数.

struct sockaddr_in{
short int sin_family; /*AF_INET*/
unsigned short int sin_port; /*Port number*/
struct in_addr sin_addr;/*Internet address*/
};

IP地址结构in_addr被定义为:

struct in_addr{
unsigned long int s_addr;
}

TCP/IP网络原理中的定义如下:

struct sockaddr_in
{
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
};
struct in_addr
{
in_addr_t s_addr; //32位IPv4地址
}
  • sin_family
    每种协议族适用的地址族均不同.如IPv4使用4字节地址族,IPv6使用16字节地址族.
  • sin_port
    以网络字节序保存16位端口号
  • sin_addr
    以网络字节序保存32位IP地址信息.
struct sockaddr
{
short int sin_family; //地址族
char sa_data[14]; //地址信息
}

此结构体成员sa_data保存的地址信息中需包含IP地址和端口号,剩余部分应填充0.结构体sockaddr并非只为IPv4设计.

struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式,二者长度一样都是16字节,二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr.一般情况下需要把sockaddr_in结构强制转换成sockaddr结构再次传入系统调用函数中.

//server
//创建一个服务器套接字,将它绑定到一个名字,然后创建一个监听队列,开始接受客户的连接.

//包含必要的头文件并设置变量:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
//#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
int server_sockfd,client_sockfd;
int server_len,client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;

//删除以前的套接字,为服务器创建一个未命名的套接字
//unlink("server_socket");
server_sockfd=socket(AF_INET,SOCK_STREAM,0);

//命令套接字
server_address.sin_family=AF_INET;
//strcpy(server_address.sun_path,"server_socket");
server_address.sin_addr.s_addr=inet_addr("127.0.0.1");
server_address.sin_port=9734;
server_len=sizeof(server_address);
bind(server_sockfd,(struct sockaddr *)&server_address,server_len);

//创建一个连接队列,开始等待客户进行连接
listen(server_sockfd,5);
while(1)
{
char ch;
printf("server waiting\n");

//接受一个连接:
client_len=sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);
//对client_sockfd套接字上的客户进行读写操作
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}

}
/*
想允许服务器和远程客户进行通信,就必须指定一组你允许连接的IP地址.可以使用特殊值INADDR_ANY,
来表示,你将接受来自计算机任何网络接口的连接
当服务端监听INADDR_ANY时,
1. 同一电脑上的客户端去connect 127.0.0.1, 是可以连接上的。
2. 或者客户端去connect 10.251.234.120(服务器), 也是可以连接上的, 此时, 不一定要求客户端和服务端处在同一台电脑上
*/
//client
//包含必要的头文件并设置变量
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
int sockfd;
int len;
struct sockaddr_in address;
int result;
char ch='A';

//为客户创建一个套接字:
sockfd=socket(AF_INET,SOCK_STREAM,0);

//命名套接字,与服务器保持一致
address.sin_family=AF_INET;
address.sin_addr.s_addr=inet_addr("127.0.0.1");
address.sin_port=9734;
len=sizeof(address);

//将我们的套接字连接到服务器的套接字:
result=connect(sockfd,(struct sockaddr *)&address,len);
if(result == -1)
{
perror("oops:client1");
exit(1);
}

//现在就可以通过sockfd进行读写操作了
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c\n",ch);
close(sockfd);
close(sockfd);
exit(0);
}

命名套接字

只有命名的套接字,才可以被其他进程使用.这样,AF_UNIX套接字就会关联到一个文件系统的路径名.

#include <sys/socket.h>
int bind(int socket,const struct sockaddr *address,size_t address_len);

bind调用成功时返回0,失败返回-1
  • socket 要分配地址信息的套接字文件描述符
  • address 存有地址信息的结构体变量地址值
  • address_len 第二个结构体变量的长度

bind系统调用把参数address中的地址分配给与文件描述符socket关联的未命名套接字.参数address_len参数传递地址结构的长度.

地址长度和格式取决于地址族.bind调用需要将一个特定的地址结构指针转换为指向通用地址类型(struct sockaddr *)

创建套接字队列

listen系统调用创建一个队列来保存未处理的请求,为了能够在套接字上接受进入的连接.只有调用了listen函数,客户端才能进入可发出连接请求的状态,这时客户端才能调用connect函数.

#include<sys/socket.h>
int listen(int socket,int backlog);

listen成功时返回0,失败返回-1
  • socket 希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)
  • backlog 连接请求队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列.

服务器端处于等待连接请求状态指客户端请求连接时,受理连接前一直使请求处于等待状态.

客户端连接请求本身也是从网络中接收到的一种数据,需要套接字接收.此时的服务器段套接字是接收连接请求的一名门卫.

linux系统可能会对队列中可以容纳的未处理连接的最大数目做出限制.因此,listen函数将队列长度设置为backlog参数值.在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字.超出的连接会被拒绝,导致客户的连接请求失败.listen函数的这种机制允许当服务器程序正忙于处理前一个客户请求的时候,将后续的客户连接放入队列等待处理,backlog参数常用的值为5.

受理客户端连接请求

调用listen函数后,若有新的连接请求,则应按序受理.受理请求意味着进入可接受数据的状态.
服务器程序创建并命名套接字之后,就可以通过accept系统调用来等待客户建立对该套接字的连接.

#include<sys/socket.h>
int accept(int socket,struct sockaddr *address,size_t *address_len);
成功时返回创建的套接字文件描述符,失败时返回-1
  • socket服务器套接字的文件描述符
  • address 保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息
  • address_len 第二个参数addr结构体的长度,但是是存有长度的变量地址.函数调用完成后,该变量即被填入客户端地址长度

accept函数受理连接请求等待队列中待处理的客户端连接请求.函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回文件描述符.套接字是自动创建的,并自动与发起连接请求的客户端建立连接.

accept系统调用只有当有客户程序试图连接到由socket参数指定的套接字上时才返回.客户程序指套接字队列中排在第一个的未处理连接.accept函数将创建一个新套接字来与该客户进行通信,并且返回新套接字的描述符.新套接字的类型和服务器监听套接字类型是一样的.

套接字必须由bind调用命名,listen调用分配一个连接队列.连接客户的地址将被放入address参数指向的sockaddr结构中.也可以将address参数指定为空指针.

address_len指定客户结构的长度.如果客户地址的长度超过这个值,它将被截断.

如果套接字队列中没有为未处理的连接,accept将阻塞(程序将暂停)直到有客户连接为止.

请求连接

服务器端调用listen函数后创建连接请求等待队列,之后客户端即可请求连接.
客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法连接到服务器.通过connect调用完成

#include<sys/socket.h>
int connect(int socket,const struct sockaddr *address,size_t address_len);

connect调用成功时返回0,失败返回-1
  • socket 客户端套接字文件描述符
  • address 保存目标服务器端地址信息的变量地址值
  • address_len 以字节为单位传递已传递给第二个结构体参数adderss的地址变量长度.

socket指定的套接字将连接到参数address指定的服务器套接字,address指向的结构的长度由参数address_len指定.参数socket指定的套接字必须是通过socket调用获得的一个有效的文件描述符.

客户端调用connect函数后,发生一下情况之一才会返回(完成函数调用)

  • 服务器端接收连接请求
  • 发生断网等异常情况而中断连接请求
    注意,所谓的”接收连接”,并不意味着服务器端调用accept函数,其实是服务器段把连接请求信息记录到等待队列.因此connect函数返回后并不立即进行数据交换.

如果连接不能立刻建立,connect调用将阻塞一段不确定的超时时间.一旦这个超时时间到达,连接将被放弃,connect调用失败.但如果connect调用被一个信号中断,而该信号又得到了处理.connect调用还是会失败,但连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立.

关闭套接字

close函数可以用来终止服务器和客户上的套接字连接,就如同关闭底层文件描述符.应该在连接的两端都关闭套接字.服务器端,应该在read调用返回0时关闭套接字.

套接字通信

网络套接字与文件系统套接字

套接字有本地套接字和网络套接字两种。本地套接字的名字是Linux文件系统中的文件名,一般放在/tmp或/usr/tmp目录中;网络套接字的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符允许Linux将进入的针对特定端口号的连接转到正确的服务器进程。

网络套接字不仅可用于局域网,任何带有因特网连接(即使是一个调制解调器拨号连接)的机器都可以使用网络套接字来彼此通信.甚至可以在一台UNIX单机上运行基于网络的程序,因为UNIX计算机通常会配置一个只包含它自身的回路(loopback)网络.回路网络对调试网络应用程序很有用,因为它排除了任何外部网络问题.

回路网络中只包含一台计算机,传统上它被称为localhost,它有一个标准的IP地址127.0.0.1(本地主机).网络主机文件/etc/hosts中列出了本地主机地址以及在共享网络中其他主机的名字和对应的地址.

每个与计算机进行通信的网络都有一个与之关联的硬件接口.一台计算机可能在每个网络中都有一个不同的网络名,就会有几个不同的IP地址.

主机字节序和网络字节序

不同CPU中,整型值在内存空间中的保存方式是不同的.保存顺序的不同意味着对接收数据的解析顺序也不同.
CPU向内存保存数据的方式有2种,这意味着CPU解析数据的方式也分为2种.

  • 大端序(Big Endian):高位字节存放到低位地址
  • 小端序(Little Endian):高位字节存放到高位地址

主机字节序代表CPU数据保存方式,在不同CPU中也各不相同.目前主流的Intel系列CPU以小端序方式保存数据.在通过网络传输数据时约定统一方式,这种约定成为网络字节序—大端序.

先把数据数组转化成大端序格式再进行网络传输.所有计算机接收数据时应识别该数据是网络字节序,小端序系统传输数据时应转化为大端序排列方式.

字节序转换

#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

htonl(host to network,long)长整数从主机字节序转换到网络字节序 用于IP地址转换
htons(host to network,short)短整型从主机字节序转换到网络字节序 用于端口号转换
如果计算机本身的主机字节序与网络字节序相同,这些函数执行空操作.

数据在传输之前都要经过转换吗?
没必要,这个过程是自动的.除了向sockaddr_in结构体变量填充数据外,其他情况无需靠需字节序问题.

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc,char *argv[])
{
unsigned short host_port=0x1234;
unsigned short net_port;
unsigned long host_addr=0x12345678;
unsigned long net_addr;

net_port=htons(host_port);
net_addr=htonl(host_addr);

printf("Host ordered port: %#x \n",host_port);
printf("network ordered port: %#x \n",net_port);
printf("Host ordered address: %#lx \n",host_addr);
printf("Network ordered address: %#lx \n",net_addr);

return 0;
}

网络地址的初始化与分配

1. 将字符串信息转换为网络字节序的整数型

sockaddr_in中保存地址信息的成员为32位整数型.inet_addr函数将字符串形式的IP地址转换成32位整数型数据.此函数在转换类型的同时进行网络字节序转换.

#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
成功时返回32位大端序整数型值,失败时返回INADDR_NONE
#include <stdio.h>
#include <arpa/inet.h>

int main(int argc,char *argv[])
{
char *addr1="1.2.3.4";
char *addr2="1.2.3.256";

unsigned long conv_addr=inet_addr(addr1);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n",conv_addr);

conv_addr=inet_addr(addr2);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n",conv_addr);

return 0;
}

inet_aton函数与inet_addr函数在功能上完全相同,也将字符串形式IP地址转换为32为网络字节序整数并返回.但使用了in_addr结构体.

#include <arpa/inet.h>
int inet_aton(const char * string,struct in_addr *addr);
成功返回1,失败返回0
  • string 含需转换的IP地址信息的字符串地址值
  • addr 将保存转换结果的in_addr结构体变量的地址值
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

void error_handling(char *message);

int main(int argc,char *argv[])
{
char *addr="127.232.124.79";
struct sockaddr_in addr_inet;

if(!inet_aton(addr,&addr_inet.sin_addr))
error_handling("Conversion error");
else
printf("Network ordered integer addr: %#x \n",addr_inet.sin_addr.s_addr);

return 0;

}
void error_handling(char *message)
{
fputs(message,stderr);
exit(1);
}

2. 网络地址初始化

struct sockaddr_in addr;
char * serv_ip=""; //声明IP地址字符串
char * serv_port=""; //声明端口号字符串
memset(&addr,0,sizeof(addr)); //结构体变量addr的所有成员初始化为addr.sin_family=AF_INET; //指定地址族
addr.sin_addr.s_addr=inet_addr(serv_ip); //基于字符串的IP地址初始化
addr.sin_port=htons(atoi(serv_port)); //基于字符串的端口号初始化

atoi函数把字符串类型的值转换成整数型.

3. 客户端地址信息初始化

服务器端的准备工作通过bind函数完成,而客户端则通过connect函数完成.服务器段声明sockaddr_in结构体变量,将其初始化为赋予服务器IP和套接字的端口号,然后调用bind函数;而客户端则声明sockaddr_in结构体,并初始化为要与之连接的服务器端套接字的IP和端口号,然后调用connect函数.

4. INADDR_ANY

INADDR_ANY等价于inet_addr(“0.0.0.0”);
当服务器的监听地址是INADDR_ANY时,会监听服务器上所有的网卡.若采用这种方式,则可自动获取运行服务器端的计算机IP地址.若同一计算机中一分配多个IP地址,则只要端口号一致,就可以从不同的IP地址接收数据.

实例1:

//server
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{

// AF_INET 表示采用TCP/IP协议族
// SOCK_STREAM 表示采用TCP协议
// 0是通常的默认情况
unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addrSrv;

addrSrv.sin_family = AF_INET; // TCP/IP协议族
addrSrv.sin_addr.s_addr = INADDR_ANY; //inet_addr("0.0.0.0");
addrSrv.sin_port = htons(8888); // socket对应的端口

// 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
bind(sockSrv,(struct sockaddr*)&addrSrv, sizeof(addrSrv));

// 将socket设置为监听模式,5表示等待连接队列的最大长度
listen(sockSrv, 5);

struct sockaddr_in addrClient;
int len = sizeof(addrClient);

while(1)
{
// sockSrv为监听状态下的socket
// &addrClient是缓冲区地址,保存了客户端的IP和端口等信息
// len是包含地址信息的长度
// 如果客户端没有启动,那么程序一直停留在该函数处
unsigned int sockConn = accept(sockSrv,(struct sockaddr*)&addrClient, &len);

char sendBuf[100] = {0};
sprintf(sendBuf,"%s", inet_ntoa(addrClient.sin_addr)); // 将客户端的IP地址保存下来
write(sockConn, sendBuf, 100); // 发送数据到客户端
char recvBuf[100] = {0};
read(sockConn, recvBuf, 100); // 接收客户端数据
printf("%s\n", recvBuf);
close(sockConn);
}

close(sockSrv);

return 0;
}

/*
sprintf 字符串格式化命令,主要功能是把格式化的数据写入某个字符串中。sprintf 是个变参函数。
原型
int sprintf( char *buffer, const char *format, [ argument] … );

参数列表
buffer:char型指针,指向将要写入的字符串的缓冲区。
format:格式化字符串。
[argument]...:可选参数,可以是任何类型的数据。
*/
//client
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{

unsigned int sockClient = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addrSrv;
addrSrv.sin_addr.s_addr = inet_addr("10.251.234.120");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8888);
int ret = connect(sockClient, (struct sockaddr*)&addrSrv, sizeof(addrSrv));

char recvBuf[100] = {0};
read(sockClient, recvBuf, 100);
printf("%s\n", recvBuf);
write(sockClient, "hello world", 11);

close(sockClient);

return 0;
}

实例2:

//server
#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); //服务器端实现过程中先要创建套接字.但此时的头啊戒子尚非真正的服务器端套接字
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)
error_handling("bind() error");

if(listen(serv_sock,5)==-1) //调用listen函数进入等待连接请求状态,连接请求等待队列的长度设置为5.此时的套接字才是服务器端套接字.
error_handling("listen() error");

clnt_addr_size=sizeof(clnt_addr);
clnt_sock=accept(serv_sock,(struct sockaddr*) &clnt_addr,&clnt_addr_size); //调用accept函数从对头取1个连接请求与客户端建立连接,并返回创建的套接字 文 //件描述符.调用accept函数时若等待队列为空,则accept函数不会返回,直到队列 //中出现新的客户端连接
if(clnt_sock==-1)
error_handling("accept() error");
write(clnt_sock,message,sizeof(message)); //调用write函数向客户端传输数据
close(clnt_sock); //close函数关闭连接
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
//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); //创建准备连接服务器端的套接字,此时创建的是TCP套接字
if(sock==-1)
error_handling("socket() error");

memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET; //结构体变量serv_addr中初始化IP和端口信息,初始化值为目标服务器端套接字的IP和端口信息
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); //接收数据后调用close函数关闭套接字,结束与服务器端的连接
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}