网络编程学习笔记---Linux+C语言(未完待续)

0.17字数 7658阅读 305

第一章 TCP/IP简介

基本的C/S服务模型

网络编程是指编写的网络通信程序可以与网络上的其他程序进行通信。

TCP/IP四层结构

网络接口层

网际层负责相邻互联网上的不同主机之间的通信,主要包括IPv4、ICMP、RIP、IGMP

传输层负责主机中两个进程之间的通信,主要包括TCP、UDP

应用层直接为用户的应用进程提供服务

套接字编程就是应用层到传输层的接口(API)

TCP协议介绍

TCP(传输控制协议)是TCP/IP体系中面向连接的运输层协议,它可以保证数据可靠的传输。

码元比特(6位):分为6个标志,置1表示有效

URG和紧急指针配合使用,发送紧急数据

ACK指出确认字段是否有效

PSH接收方应该尽快将这个报文提交给应用层

RST重建连接

SYN同步序号用来请求建立连接

FIN用来释放连接

TCP连接建立与终止

TCP是面向连接的协议。TCP连接的建立和释放是每一次通信中必不可少的过程。
TCP连接的建立需要经过三次数据传输。也就是三次握手

建立TCP连接
  1. 服务器准备好接收客户的连接请求 -> socket、bind、listen函数
  2. 客户主动打开 -> connect函数,SYN分节(用来请求建立连接)
  3. 服务器收到客户端发来的SYN分节后,必须发送ACK对其确认,同时发送SYN分节给客户端,表示接受客户端建立连接的请求 -> SYN分节,ACK(指出确认字段是否有效)
  4. 客户端发送ACK确认服务器的SYN -> ACK(指出确认字段是否有效)
  5. 连接建立成功
释放TCP连接
  1. 客户端主动关闭连接 -> close函数,FIN分节
  2. 服务器收到FIN分节后执行被动关闭,并关闭套接字 -> 发送ACK(对客户端的FIN分进行确认),close函数,FIN分节
  3. 客户端接受到FIN分节后,发送ACK确认分节后,彻底关闭连接

TCP连接中的分组交换

TCP连接中的分组交换

UDP协议介绍

UDP(用户数据报协议)是面向无连接的服务,提供不可靠的数据传输。

第二章 套接字编程简介

套接字基础

套接字是一种网络API(应用程序编程接口),可以用它来开发网络程序

套接字接口提供一种进程间通信的方法,使得在相同或不同的主机上的进程能以相同的规范进行双向信息传送

套接字接口是应用层到传输层的接口

套接字类型

套接字类型是指创建套接字的应用程序要使用的通信服务的类型。

最常用的几种类型:

  • SOCK_STREAM:流式套接字,提供面向连接、可靠的数据传输服务,数据是按字节流、按照顺序收发,保证数据在传输过程中无丢失、无冗余。TCP支持该套接字
  • SOCK_DGRAM:数据报套接字,提供面向无连接的服务,数据收发无序,不能保证数据的准确到达。UDP支持该套接字
  • SOCK_RAW:原始套接字。允许对低于传输层的协议或物理网络直接访问,例如可以接收和发送ICMP报(网络层的协议)。常用于检测新的协议。

套接字地址结构

IPv4套接字地址结构

#include <netinet/in.h>

typedef uint32_t in_addr_t; //无符号32位整数,IPv4地址
typedef uint16_t in_port_t; //无符号16位整数,TCP或UDP端口
typedef unsigned short sa_family_t; //套接字地址结构的地址族 unsigned short 0~65535字节

struct in_addr{
    in_addr_t s_addr; //s_addr成员存储的是网络字节序的32位IPv4地址
};

struct sockaddr_in{
    uint8_t sin_len; //长度成员,存储套接字地址结构的长度(一般不设置)
    sa_family_t sin_family; //sin_family是Internet地址族,在IPv4中是AF_INET
    in_port_t sin_port; //端口号,以网络字节序存储
    struct in_addr sin_addr; //是一个结构,该结构中的成员存储的才是IP地址
    char sin_zero[8]; //未使用,置0
};

举个例子:

struct sockaddr_in ser;
ser.sin_addr给出的是一个存放地址的结构
ser.sin_addr.s_addr存储的是地址中的内容,也就是IP地址的值

IPv6套接字地址结构

#include <netinet/in.h>
typedef uint16_t in_port_t;
typedef unsigned short sa_family_t;

struct in6_addr{
    uint8_t s6_addr[16];
};

struct sockaddr_in6{
    uint8_t sin6_len; //长度成员
    sa_family_t sin6_family; //Internet地址族,在IPv6中是AF_INET6
    in_port_t sin6_port; //端口号,以网络字节序存储
    uint32_t sin6_flowinfo; //低24位是流量标号,下4位是优先级,再下4位保留
    struct in6_addr sin6_addr; //in6_addr结构中的s6_addr成员,存储的是网络字节序的128位IPv6地址
};

两种套接字地址结构的比较

两种套接字地址结构的比较

通用套接字地址结构

套接字地址结构作为参数传递给任一个套接字函数时,通常通过指针来传递

当套接字函数取得此参数时,参数中可能存放的是来自所支持的任何协议族的地址结构。因此在调用套接字函数时,需要将指向特定协议的地址结构的指针类型转换成指向通用的地址结构的指针。

通用套接字地址结构如下:

#include <sys/socket.h>
struct sockaddr{
  uint8_t sa_len;
  sa_family_t sa_family;
  char sa_data[14];
};

套接字基本函数

字节排序函数

广域网规定的网络字节序采用大端字节序

  • 小端字节序:将低序字节存储在起始地址
  • 大端字节序:将高序字节存储在起始地址
小段字节序和大端字节序

将某给定主机所使用的字节序称为主机字节序。为了使采用不同字节序的主机能够互相通信,TCP/IP协议规定了网络字节序。

所有主机或路由器在发送IP数据包之前要首先将相应的信息转换成网络字节序。相应的,在接收数据包后,要将网络字节序转换成主机字节序。

主机字节序和网络字节序之间的相互转换,要用到以下四个函数:

#include <netinet/in.h>
uint16_t htons(uint16_t hosts);
uint32_t htonl(uint32_t hostl);
uint16_t ntohs(uint16_t nets);
uint32_t ntohl(uint32_t netl);

在上述四个函数中,h代表主机host,n代表网network,s代表短整型short,l代表长整型long

  • htons:将16位的短整型数,从主机字节序转换成网络字节序
  • htonl:将32位的长整型数,从主机字节序转换成网络字节序
  • ntohs:将16位的短整型数,从网络字节序转换成主机字节序
  • ntohl:将32位的长整型数,从网路字节序转换成主机字节序

字节操纵函数

以字母b(byte)打头

#include <string.h>
void bzero(void *dest, size_t len); 
//bzero函数将目标中指定数目的字节置为0,经常用此函数来对套接字地址结构进行初始化
void bcopy(const void *src, void *dest, size_t len);
 //bcopy函数将指定数目的字节从源拷贝到目标
int bcmp(const void *src, void *dest, size_t len);
 //bcmp函数比较源和目标两个字符串,若相同返回值为0,否则返回非0值

以mem(memory)打头

void *memset(void *dest, int x, size_t len); 
//将目标指定数目的字节置为值x
void *memcpy(void *dest, const void *src, size_t len);
//将指定数目的字节从源拷贝到目标
int memcmp(const void *str1, const void *str2, size_t len); 
//比较两个字符串,若相同返回值为0,否则返回非0值。如果str1所指字节大于str2所指字节,则返回值大于0,否则返回值小于0

IP地址转换函数

用于字符串的IP和二进制值的IP相互转换

#include <arpa/inet.h>
in_addr_t inet_addr(const char *str); 
int inet_aton(const char *str, struct in_addr *numstr);
char *inet_ntoa(struct in_addr inaddr);

a代表ASCII串,n代表数值格式,是存在于套接字地址结构中的二进制值。

inet_addr函数

将字符串形式的IP地址转换成32位二进制值的IP地址。str指向字符串形式的IP地址。函数调用成功,返回值为32位二进制值的IP地址。

inet_aton函数

将字符串形式的IP地址转换成32位二进制值的IP地址。str指向字符串形式的IP地址。numstr指向转换后的32位网络字节序的IP地址。如果成功返回1,否则返回0

inet_ntoa函数

将一个32位网络字节序的二进制值的IP地址转换成相应的点分十进制的IP地址。这个函数的参数是一个结构,而不是指向结构的指针。该函数的返回值所指向的串留在静态内存中,所以函数是不可重入的


#include <arpa/inet.h>
int inet_pton(int family, const char *str, void *numstr);
const char *inet_ntop(int family, const void *numstr, char *str, size_t len);

p代表地址的表达格式是ASCII串。n代表数值格式,是存在于套接字地址结构中的二进制值。

这两个函数中的family参数,指的是操作地址的地址族,IPv4是AF_INET,IPv6是AF_INET6

inet_pton函数

将指针str所指的字符串形式的IP地址,转换成网络字节序的二进制值的IP地址,并用指针numstr存储。如果成功返回1,如果对于指定的family输入的字符串不是一个有效的表达格式,则返回值为0,出错返回-1

inet_ntop函数

将numstr所指的二进制值的IP地址转换成字符串形式的IP地址,并用指针str存储。参数len是目标的大小,为了避免函数溢出其调用者的缓冲区。

五个地址转换函数

五个地址转换函数

isfdtype函数

用于测试某个描述符是不是给定的类型

#include <sys/stat.h>
int isfdtype(int fd, int fdtype);

isfdtype函数:测试描述符fd是不是fdtype参数指定的类型

为了测试描述符是否是套接字描述符,fdtype参数应设为S_IFSOCK。例如:

isfdtype(sockfd, S_IFSOCK);

值-结果参数

当把套接字地址结构传递给套接字函数时,总是通过指针来传递的,即传递的是一个指向套接字地址结构的指针,结构的长度也可用参数传递。

从进程到内核传递套接字地址结构的函数: bind、connect、sendto

在这三个函数的参数中都含有两个相似的参数,分别是指向套接字地址结构的指针及该地址结构的大小。例如:

struct sockaddr_in ser;
bind(sockfd, (struct sockaddr *)&ser, sizeof(ser));

这里的bind函数将套接字地址结构和结构大小都传递给了内核,所以进程到内核拷贝的数据长度是确定的


从内核到进程传递套接字地址结构的函数: accept、recvfrom、getsockname、getpeername

这四个函数包含的两个参数分别是指向套接字地址结构的指针和指向套接字地址结构大小的指针。例如:

struct sockaddr_in client;
socklen_t len;
len = sizeof(client);
accept(listenfd, (struct sockaddr *)&client, &len);

由于accept函数中存放套接字地址结构长度的参数是个指针,那么在函数调用时,结构长度是个值,这个值告诉内核该结构的长度,使内核在写这个结构时不会越界。而当函数返回时,结构大小的值发生了变化,变成了内核在此结构中确切存储的数据长度。进程可以通过这个值得到内核写入多少信息到这个结构中。这种在参数传递的过程中,其值发生变化了的参数称为值-结果参数,内核和进程间的两种传递参数的方式如下图:

内核和进程间传递参数

第三章 基本TCP套接字编程

3.1 TCP套接字编程

使用TCP套接字编程可以实现基于TCP/IP协议的面向连接的通信,它分为服务器和客户端两部分:

TCP客户/服务器的套接字函数

TCP套接字编程中,服务器端实现的步骤:

  1. 使用socket()函数创建套接字
  2. 使用bind函数为创建的套接字绑定到指定的地址结构
  3. listen()函数设置套接字为监听模式,使服务器进入被动打开的状态
  4. 接受客户端的连接请求,建立连接
  5. 接收、应答客户端的数据请求
  6. 终止连接

客户端实现的步骤:

  1. 使用socket()函数创建套接字
  2. 调用connect()函数建立一个与TCP服务器的连接
  3. 发送数据请求,接收服务器的数据应答
  4. 终止连接

socket函数

为了执行网络I/O,无论是服务器还是客户端,首先必须调用socket函数,产生TCP套接字,作为TCP通信的传输端点

#include <sys/socket.h>
int socket(int family, int type, int protocol);

socket函数中family参数指明协议族。type参数指明产生套接字的类型。protocol参数是协议标志,一般在调用socket函数时将其置为0,但如果是原始套接字,就需要为protocol指定一个常值。

该函数调用成功,返回一个小的非负的整数值,它与文件描述符类似,这里称之为套接字描述符,简称套接字,之后的I/O操作都由该套接字完成。如果函数调用失败,则返回-1。

family参数指明的协议族,确定了socket使用的协议类型,值通常为:

  • AF_INET: IPv4协议
  • AF_INET6: IPv6协议
  • AF_ROUTE: 路由套接口

type参数指明产生套接字的类型,它常用的值包括:

  • SOCK_STREAM: 字节流套接口,TCP使用的是这种格式
  • SOCK_DGRAM: 数据报套接口,UDP使用的是这种形式
  • SOCK_RAW: 原始套接口

并不是所有的family和type的组合都有效,下表中Yes表示组合有效,No表示组合无效:

sock函数中协议族和套接字类型的组合

调用sock函数的代码如下:

#include <sys/socket.h>
......
int sockfd;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
    //handle exception
    ......
}

connect函数

connect函数用于激发TCP的三次握手过程。建立与远程服务器的连接

TCP客户端使用connect函数来配置套接字,建立一个TCP服务器的连接

connect函数如下:

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

sockfd参数是由socket函数返回的套接字描述符

addr参数是指向服务器的套接字地址结构的指针,如果是IPv4地址,server指向的就是一个sockaddr_in地址结构,在进行connect函数调用时,必须将sockaddr_in结构转换成通用地址结构sockaddr。最后一个参数addrlen是该套接字地址结构的大小。

调用成功返回0,出错则返回-1

如果描述符是TCP套接字,调用函数connect就是建立一个TCP的连接,只在连接建立成功或者出错时该函数才返回,返回的错误有如下几种情况:

  • 如果客户没有收到SYN分节的响应,返回ETIMEDOUT,这可能需要重发若干次SYN
  • 如果对客户的SYN的响应是RST,则表明该服务器主机在指定的端口上没有进程在等待与之相连。客户端马上返回错误ECONNREFUSED
  • 如果客户发出的SYN在中间路由器上引发一个目的地不可达ICMP错误,客户端内核保存此消息,并按第一种情况,连续重传SYN,直到规定时间的超时时间,对方仍没有响应,则返回保存的消息(即ICMP错误)EHOSTUNREACH或ENETUNREACH错误返回给进程

对于TCP连接的状态,connect导致客户端从CLOSED状态转到了SYN_SENT状态。

若建立连接成功,也就是connect调用成功,状态会再变到ESTABLISHED状态。若函数connect调用失败,则套接字不能再使用,必须关闭。如果想继续向服务器发起建立连接的请求,就需要重新调用socket函数,生成新的套接字

调用connect函数的代码如下:

#include <sys/socket.h>
......
int socked;
struct sockaddr_in server;
......
bzero(&server, sizeof(server));//为套接字地址结构server设置初始值0
server.sin_family = AF_INET; //为套接字地址结构中的成员赋值
server.sin_port = htons(1234); //为套接字地址结构中的成员赋值,端口号为1234
server.sin_addr.s_addr = inet_addr("127.0.0.1"); 
//为套接字地址结构中的成员赋值,127.0.0.1是客户端要建立连接的服务器的IP地址
if(connect(sockfd, (struct sockaddr *)&server, sizeof(server) == -1) 
//调用connect函数,与服务器建立连接,connect函数第二个参数为将IPv4的套接字地址结构强制转换为通用地址结构
{
    //handle exception //如果调用connect函数失败,连接失败的异常处理
    ......
}
......

bind函数

绑定函数bind的作用就是为调用socket函数产生的套接字分配一个本地协议地址,建立地址与套接字的对应关系

对于网际协议,协议地址包括32位的IPv4地址或128位的IPv6地址和16位的UDP或TCP的端口号

对于绑定操作,地址信息必须是唯一的,在实际应用中,通过绑定的端口号来保证地址的唯一性

bind函数如下:

#include <sys/socket.h>
int bind(int socked. const struct sockaddr *server, socklen_len addrlen);

参数sockfd是套接字函数返回的套接字描述符

参数server是指向特定于协议的地址结构的指针,指定用于通信的本地协议地址

参数addrlen指定了该套接字地址结构的长度

如果调用成功返回0,调出错返回-1,并置错误号errno

对于绑定的套接字地址结构,可以指定端口号或IP地址中的任意一个,可以两个都指定,也可以一个也不指定。如果不绑定任何端口,当调用connect或listen时,内核会为套接字选择一个临时的端口。

进程如果绑定了一个特定的本地IP地址到它的套接字上,对于TCP客户端,这就为在此套接字上发送的IP数据报分配了源IP地址。对于TCP服务器端,这就限制了该套接字只接收目的地址为此IP地址的客户连接

函数绑定的IP地址、端口号

bind函数的代码如下:

#include <sys/socket.h>
......
int sockfd;
int port = 1234;//bind的端口号为1234
struct sockaddr_in server;
......
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址是一个通配地址,由内核选择IP地址。关于INADDR_ANY见上图
if(bind(sockfd, (struct sockaddr *)&server, sizeof(server) == -1))
{
    //handle exception
    ......
}
......

listen函数

当调用函数socket创建一个套接字时,默认情况下它是一个主动套接字,也就是一个将调用connect函数发起连接的客户端套接字。所以对于TCP服务器,在绑定操作后,必须要调用listen函数,将这个未连接的套接字转换成被动套接字,监听有无客户要连接,进入被动接受连接请求状态

在调用listen函数后,服务器的状态从CLOSED转换到了LISTEN状态

listen函数如下:

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

参数sockfd是要设置的描述符

参数backlog规定了请求队列中的最大连接个数,它对队列中等待服务请求的数目进行了限制,如果一个服务请求到来时,输入队列已满,该套接字将拒绝连接请求。

函数调用成功返回0,出错返回-1,并置errno值

TCP三次握手中两个队列的位置
文字补充

listen函数代码如下:

#include <sys/socket.h>
......
int sockfd;
int BACKLOG = 5; //设置监听的最大连接数为5
......
if(listen(sockfd, BACKLOG) == -1) //调用listen函数,将sockfd描述符设置为监听描述符
{
    //handle exception //当listen函数调用失败时的异常处理
    ......
}
......

accept函数

accept函数使服务器接受客户端的连接请求

它将完成队列中的队头条目返回给进程,并产生一个新的套接字描述符——已连接套接字

当已完成队列为空,则进程睡眠,直到有已完成连接到达时。

accept函数如下:

#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *client, socklen_t *addrlen);

listenfd函数是由socket函数产生的套接字描述符,在调用accept函数前,已经调用listen函数将此套接字变成了监听套接字

client和addrlen参数用来返回连接对方的套接字地址结构和对应的结构长度

addrlen参数是一个值—结果参数,调用前,将addrlen指针所指的值置为client所指的套接字地址结构的长度。函数返回时,此整数值变为内核写入此套接字地址结构的准确字节数。

函数调用成功时,可以得到三个值:

  • 一个是accept函数的返回值,已连接套接字描述符。已连接套接字描述符是内核为每个被接受的客户都分别创建一个。用完则关闭。监听描述符负责接收客户的连接请求,而已连接描述符负责与对应的客户进行数据传输
  • 由client参数返回客户端的协议地址,包括IP地址和端口号等
  • 由addrlen参数返回客户端地址结构的大小

如果对客户的协议地址和地址结构的长度不感兴趣,可以将client和addrlen两个参数都设为空指针。

如果函数调用失败,accept函数将返回-1,并置errno值。调用accept函数的代码如下:

#include <sys/socket.h>
......
int listenfd, connfd; 
//定义了两个套接字描述符,一个是监听套接字描述符,一个是已连接套接字描述符

struct sockaddr_in client;
socklen_t addrlen;
addrlen = sizeof(client); //得到client当前的长度
......
connfd = accept(listenfd, (struct sockaddr *)&client, &addrlen);
//调用accept函数,接收连接请求,返回已连接套接字描述符
//与服务器连接的客户端的协议地址可以通过参数client得到,addrlen返回内核写入client结构体中的准确字节数
if(connfd == -1){
    //handle exception
    ......
}
......

数据传输函数

服务端和客户端连接建立成功后,就可以进行双向的数据传输。服务端和客户端使用各自的套接字描述符进行读写操作

write函数

write()函数用于数据的发送,如下:

#include <unistd.h>
int write(int sockfd, char *buf, int len);

参数sockfd是套接字描述符。对于服务器是accept()函数返回的已连接套接字描述符。对于客户端是调用socket()函数返回的套接字描述符。参数buf是指向一个用于发送信息的数据缓冲区。len指明传送数据缓冲区的大小。

函数滴啊用成功返回大于0的整数,也就是发送的字节数。出错则返回-1。

read函数

用于数据的接收

#include <unistd.h>
int read(int sockfd, char *buf, int len);

参数sockfd是套接字描述符。对于服务器是accept()函数返回的已连接套接字描述符。对于客户端是调用socket()函数返回的套接字描述符。参数buf是指向一个用于接收信息的数据缓冲区。len指明传送数据缓冲区的大小。

函数滴啊用成功返回大于0的整数,也就是接收的字节数。出错则返回-1。

调用read()函数的代码如下:

#include <unistd.h>
#include <sys/socket.h>
#define MAXDATASIZE 100 //定义接收信息的数据缓冲区的长度
......
int num, connfd;
char buf[MAXDATASIZE];
......
if( (num = read(connfd, buf, MAXDATASIZE)) > 0){ //接收数据,整数num返回接收的字节数
    //handle data //处理收到的数据
    ......
}
......

send函数

用于数据的发送操作

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

前三个参数与write相同,参数flags是传输控制标志,其值定义如下图:


send函数的参数flags

recv函数

用于数据的发送操作

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv函数的参数flags

调用recv函数的代码如下:

#include <unistd.h>
#include <sys/socket.h>
#define MAXDATASIZE 100
......
int num, connfd;
char buf[MAXDATASIZE];
......

if( (num = recv(connfd, buf, MAXDATASIZE)) > 0){ //接收数据,整数num返回接收的字节数
    //handle data //处理收到的数据
    ......
}
......

TCP套接字编程实例

程序实现的功能是:

  • 客户根据用户提供的IP地址,连接到相应的服务器
  • 服务器等待客户的连接,一旦连接成功,则显示客户的IP地址、端口号,并向客户发送字符串
  • 客户接受服务器发送的信息并显示

TCP服务器端程序如下(server.c):

#include <stdio.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h> //通用套接字地址结构
#include <netinet/in.h> //IPv4套接字地址结构
#include <arpa/inet.h> //IP地址转换函数
#include <string.h>
#include <stdlib.h>

#define PORT 1234 //客户端与服务器的端口要对应
#define BACKLOG 1//listen函数中的参数,此参数规定了请求队列中的最大连接个数,由于本例不是并发服务器,所以最大允许连接的数量BACKLOG定义为1

main()
{
    int listenfd, connectfd/*已连接套接字描述符*/;
    struct sockaddr_in server;//套接字地址结构
    struct sockaddr_in client;
    socklen_t addrlen; 

    /*Create TCP socket*/
    if ((listenfd=socket(AF_INET, SOCK_STREAM, 0)) == -1) 
    //创建TCP套接字,socket()函数第二个参数为字节流接口。如果出错打印错误信息。
    {
        perror("socket() error.");
        exit(-1);
    }

    int opt = SO_REUSEADDR;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    //以上两行设置套接字选项SO_REUSEADDR,即地址重用选项。
    //由于系统默认是只允许一个套接字绑定一个特定的协议地址上,并且当该套接字关闭后,系统仍不允许在该地址上绑定其他套接字。
    //如果去掉这两行,程序运行时产生的错误信息为:"Bind() error:Address already in use"

    bzero(&server, sizeof(server));//初始化server套接字地址结构,初始值为0
    server.sin_family = AF_INET;//为套接字地址结构中的成员赋值
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    //调用bind()函数将套接字描述符与server套接字地址结构中的协议地址绑定
    {
        perror("bind() eror");
        exit(1);
    }

    if (listen(listenfd, BACKLOG) == -1) //listen()函数将listenfd描述符设置为监听套接字,等待客户连接
    {
        perror("listen() error.\n");
        exit(1);
    }

    addrlen = sizeof(client);//得到client当前的长度
    if ((connectfd = accept(listenfd, (struct sockaddr *)&client, &addrlen)) == -1)
    //接受客户端连接,将客户的地址信息存放在client地址结构中,&addrlen为内核写入client结构体中的准确字节数
    {
        perror("accept() error\n");
        exit(1);
    }
    printf("You got a connection from client's ip is %s, port is %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port));
    //显示客户的IP地址和端口号,通过inet_ntoa()函数将IP地址转换成可显示的ASCII串,通过htons()函数将端口号转换成网络字节序
    send(connectfd, "Welcome\n",8,0);//发送Welcome字符串给客户端
    close(connectfd);//先关闭已连接套接字,再关闭监听套接字
    close(listenfd);
    return 0;
}

TCP客户端程序如下(client.c):

#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>

#define PORT 1234 //服务器的端口与客户端的端口对等
#define MAXDATASIZE 100 //这里的缓冲区采用静态方式分配

int main(int argc, char *argv[])
{
    int sockfd, num;
    char buf[MAXDATASIZE];
    struct hostent *he;
    struct sockaddr_in server;
    if (argc != 2) //检查用户的输入。如果用户输入不正确,提示用户正确的输入方式
    {
        printf("Usage:%s <IP Address>\n", argv[0]);
        exit(1);
    }

    //通过用户输入的点分十进制形式的IP地址,获得服务器的相关地址信息
    if ((he = gethostbyname(argv[1])) == NULL)
    {
        printf("gethostbyname() error\n");
        exit(1);
    }

    //调用socket()函数产生套接字描述符
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("socket() error\n");
        exit(1);
    }

    //初始化服务器的地址结构,并为地址结构的成员赋值
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr = *((struct in_addr *)he->h_addr);

    //调用connect()函数连接到服务器server
    if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    {
        printf("connect() error\n");
        exit(1);
    }

    //接受服务器发过来的字符串,并保存在buf中。接收的真正字节数被存储在num中
    if ((num = recv(sockfd, buf, MAXDATASIZE,0)) == -1)
    {
        printf("recv() error\n");
        exit(1);
    }
    buf[num-1] = '\0'; //以\0标志字符串的结束
    printf("server message: %s\n", buf);//显示从服务器接收到的buf中的信息
    close(sockfd); //关闭套接字
}

代码运行截图:

运行截图

第四章 基本UDP套接字编程

UDP套接字编程

UDP客户/服务器的套接字函数

UDP套接字编程中,服务器端实现的步骤:

  • 使用socket()函数创建套接字
  • 为创建的套接字绑定到指定的地址结构
  • 等待接受客户端的数据请求
  • 处理客户端的请求
  • 向客户端发送应答数据
  • 关闭套接字

客户端实现的步骤:

  • 使用socket()函数创建套接字
  • 发送数据请求给服务器
  • 等待接收服务器的数据应答
  • 关闭套接字

recvfrom函数

此函数用于接收数据,函数中药指明源地址

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, size_t *addrlen);
//前三个参数分别为:调用socket函数生成的描述符、指向读入缓冲区的指针、读入的字节数
//flags参数是传输控制标志,其值通常为0,代表所做的操作与read相同,还有MSG_OOB, MSG_PEEK
//from返回与之通信的对方的套接字地址结构,告诉用户接收到的数据报来自于谁
//addrlen是一个指向整数值的指针(值-结果参数),存储数据发送者的套接字地址结构的字节数
//如果from或addrlen两者之一要为空,必须同时设为空

函数调用成功的返回值为接收到数据的长度(以字节为单位),也就是接收的数据报中用户数据的总量。调用失败返回-1,并置errno

调用recvfrom函数代码如下:

#include <sys/types.h>
#include <sys/socket.h>
#define MAXDATASIZE 100
......
int num, sockfd;
socklen_t addrlen;
sockaddr_in peer_addr;
char buf[MAXDATASIZE];
......
addrlen = sizeof(peer_addr);
while(1)
{
    num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&peer_addr, &addrlen);
    //用recvfrom函数接收数据,将接收到的数据保存在buf中,整数num返回接收的字节
    //地址结构peer_addr返回发送数据方的协议地址,addrlen返回存储在peer_addr中的字节数
    if (num < 0)
    {
        /*handle exception*/
    }
    /*handle data*/
    ......
}
......

sendto函数

此函数用于发送数据,要指明目的地址

sendto函数如下:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *to, int addrlen);
//前三个参数分别为:调用socket函数生成的描述符、指向发送缓冲区的指针、发送的字节数
//flags参数是传输控制标志,其值通常为0,代表所做的操作与write相同,还有MSG_DONTROUTE,MSG_OOB
//函数sendto的参数to的类型是套接字地址结构,指明数据将发往的协议地址,它的大小由addrlen参数决定

该函数调用成功的返回值为发送数据的长度(以字节为单位)。如果调用失败则返回-1,并置相应的errno值

UDP套接字编程实例

程序实现的功能是:

  • 客户根据用户提供的IP地址,将用户从终端输入的信息发送给服务器,然后等待服务器的回应
  • 服务器接收客户端发送的信息,并显示,同时显示客户的IP地址、端口号,并向客户端发送信息。如果服务器接收的客户信息为“bye”,则退出循环,并关闭套接字。
  • 客户接收、显示服务器发回的信息,并关闭套接字

UCP服务器端程序如下(UDP_server.c):

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 1234 //定义端口号
#define MAXDATASIZE 100 //定义接收缓冲区大小
main()
{
    int sockfd;
    struct sockaddr_in server;
    struct sockaddr_in client;
    socklen_t len;
    int num;
    char buf[MAXDATASIZE];
    /*creating UDP socket*/
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {//调用socket函数,产生UDP套接字。如果出错打印错误信息。
        perror("Creating socket failed.");
        exit(1);
    }
    bzero(&server, sizeof(server));//初始化server套接字地址结构,并对地址结构中的成员赋值
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    {//将套接字和指定的协议地址绑定
        perror("Bind() error.");
        exit(1);
    }
    len = sizeof(client);
    while(1)
    {
        num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&client, &len);
        //接收客户端的信息,并存放在buf中,客户端的地址信息存放在client地址结构中。如果成功num返回接收的字符串的长度。
        if (num < 0)
        {
            perror("recvfrom() error\n");
            exit(1);
        }
        buf[num]='\0';
        printf("You got a message <%s> from client.\nIt's ip is %s, port is %d.\n", buf, inet_ntoa(client.sin_addr), htons(client.sin_port));
        //显示接收到的客户信息、客户的IP地址和端口号。通过inet_ntoa()函数将IP地址转换成可显示的ASCII串,通过htons()函数将端口号转换成网络字节序
        sendto(sockfd, "Welcome\n", 8, 0, (struct sockaddr *)&client, len);//发送Welcome字符串给客户端
        if (!strcmp(buf, "bye")) //如果客户端发来的字符串是"bye",则退出循环
        {
            break;
        }
    }
    close(sockfd);
}

UCP客户端程序如下(UDP_client.c):

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define PORT 1234 //这里的端口号要和服务器的端口号一样
#define MAXDATASIZE 100
int main(int argc, char const *argv[])
{
    int sockfd, num;
    char buf[MAXDATASIZE];
    struct hostent *he;
    struct sockaddr_in server, peer;
    socklen_t len;
    if (argc != 3) //检查用户的输入
    {
        printf("Usage: %s <IP Address><message>\n", argv[0]);
        exit(1);
    }
    if ((he = gethostbyname(argv[1])) == NULL)
    {
        printf("gethostbyname() error\n");
        exit(1);
    }
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1){
        //调用socket()函数产生套接字描述符
        printf("socket() error\n");
        exit(1);
    }
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr = *((struct in_addr *)he->h_addr); /*he->h_addr the first ip*/
    sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&server, sizeof(server));
    //将用户从命令行输入的消息发送给服务器server
    len = sizeof(server);
    while(1)
    {
        if ((num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&peer, &len)) == -1)
        //接收服务器发过来的字符串,并保存在buf中。接收的真正字节数被存储在num中,同时peer返回接收服务器的地址
        {
            printf("recvfrom() error\n");
            exit(1);
        }
        if (len!= sizeof(server) || memcmp((const void *)&server, (const void *)&peer, len) != 0)
        //由于UDP套接字是无连接的,它可能接收到其他服务器发来的信息,所以应判断信息是否来自于相应的服务器。
        //首先,比较recvfrom()函数调用后返回的地址长度len是否等于结构体server的长度。如果不是,则说明消息来自于其它服务器。
        //然后判断server和peer变量中的内容是否一致。如果一致,则说明收到的消息来自于相应的服务器。
        //注意,server和peer使用memcmp函数进行比较时,首先应转换成常量指针才能使用。
        {
            printf("Receive message from other server.\n");
            continue;
        }
        buf[num] = '\0';
        printf("Server Message: %s.\n", buf);//显示来自于服务器的信息
        break;
    }
    close(sockfd);
}

代码运行截图

可以多个客户端同时向服务器发送消息


UDP代码运行截图

connect函数用于UDP

UDP调用connect函数,没有三路握手过程,内核只是记录与之通信的对方的IP地址和端口号,它们包含在传递给connect的套接口地址结构中,并立即返回给调用进程

调用了connect函数的UDP套接字为已连接UDP套接字

UDP程序调用了connect函数,将指定与之通信的对方的IP地址和端口号,只与唯一对方通信,只能使用recv或read接收数据,只能使用send或write发送数据

详细介绍:


详细介绍

第5章 并发服务器

两种并发技术: 多进程多线程

可以同时处理多个客户请求的服务器称为并发服务器

Linux提供三种方式支持并发: 进程线程I/O多路复用

TCP并发服务器:

TCP并发服务器

多进程并发服务器

进程基础

进程 是执行中的计算机程序,是在执行过程中不断变化的动态的实体。进程是独立的,未经允许,一个进程不能访问另一个进程的资源,一个进程的崩溃不会造成其他进程崩溃。

进程创建

可以调用forkvfork函数来创建新进程。在创建新进程时,要进行资源拷贝。Linux有三种资源拷贝的方式:

  • 共享:新老进程共享通用的资源,共用一个数据结构
  • 直接拷贝:将父进程的文件、文件系统、虚拟内存等结构直接拷贝到子进程中。子进程创建后,父子进程拥有相同的结构
  • Copy on Write:把真正的虚拟内存拷贝推迟到两个进程中的任一个试图写虚拟页的时候。如果某虚拟内存页上没有出现写的动作,父子进程就一直共享该页而不用拷贝

fork函数

fork用于普通进程的创建,采用Copy on Write方式

#include <unistd.h>
pid_t fork(void);

函数调用失败返回-1,失败原因:

  • 系统中已经有太多的进程
  • 该实际用户ID的进程总数超过了系统限制

函数调用成功,返回两次:

  1. 父进程中,返回值是新派生的子进程的ID号
  2. 子进程中,返回值为0

为什么在fork的子进程中返回的是0,而不是父进程id?

原因在于: 所有子进程都只有一个父进程,它可以通过调用getppid函数来得到父进程的ID,而对于父进程,它有很多个子进程,它没有办法通过一个函数得到各子进程的ID。如果父进程想跟踪所有子进程的ID,它必须记住fork的返回值

fork函数的用法如下:

......
pid_t pid;
if((pif = fork()) > 0)
{
     //parent process
}
else if(pid == 0)
{
    //child process
    exit(0); //子进程必须用exit函数退出
}
else
{
    printf("fork() error\n");
    exit(0);
}
......

vfork函数

vfork采用共享的方式创建,新老进程共享同样的资源,完全没有拷贝

当使用vfork()创建新进程时,父进程将被暂时阻塞,而子进程则可以借用父进程的地址空间运行。这个奇特状态将持续直到子进程要么退出,要么调用execve(),至此父进程才继续执行。

#include <unistd.h>
pid_t vfork(void);

函数调用失败返回-1

函数调用成功,返回两次:

  1. 父进程中,返回值是新派生的子进程的ID号
  2. 子进程中,返回值为0

通过下面的程序来比较fork和vfork的不同

使用vfork函数

#include <sys/types.h>
#include <unistd.h>
int main(void)
{
    pid_t pid;
    int status;
    if ((pid = vfork()) == 0) //产生子进程
    {
        sleep(2);
        printf("child running.\n");
        printf("child sleeping.\n");
        sleep(5);
        printf("child dead.\n");
        exit(0);
    }
    else if (pid > 0)
    {
        printf("parent running.\n");
        printf("parent exit.\n");
        exit(0);
    }
    else
    {
        printf("fork error.\n");
        exit(0);
    }
}

运行结果:

vfork函数

使用fork函数

#include <sys/types.h>
#include <unistd.h>
int main(void)
{
    pid_t pid;
    int status;
    if ((pid = fork()) == 0) //产生子进程
    {
        sleep(2);
        printf("child running.\n");
        printf("child sleeping.\n");
        sleep(5);
        printf("child dead.\n");
        exit(0);
    }
    else if (pid > 0)
    {
        printf("parent running.\n");
        printf("parent exit.\n");
        exit(0);
    }
    else
    {
        printf("fork error.\n");
        exit(0);
    }
}

运行结果:

fork函数

进程终止

进程终止存在两种可能: 父进程先于子进程终止子进程先于父进程终止

如果父进程在子进程之前终止,则所有子进程的父进程被改为init进程,就是由init进程领养。在一个进程终止时,系统会逐个检查所有活动进程,判断这些进程是否是正要终止的进程的子进程。如果是,则该进程的父进程ID就更改为1(init的ID)。这就保证了每个进程都有一个父进程

补充知识

父进程可以通过调用wait()或waitpid()函数,获得子进程的终止信息。

wait函数

#include <sys/wait.h>
pid_t wait(int *statloc);
wait函数

wait函数的用法如下:

pid_t pid;
if((pid = fork()) > 0)
{
    ...... //parent process
    int chdstatus;
    wait(&chdstatus);
}
else if(pid == 0)
{
    ...... //child process
    exit(0);
}
else
{
    printf("fork() error\n");
    exit(0);
}

waitpid函数

此函数对等待哪个进程终止及是否采用阻塞操作方式方面给了更多的控制

waitpid函数如下:

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int option);

当参数pid=-1,option=0时,该函数等同于wait()函数

参数pid指定了父进程要求知道哪些子进程的状态:

  • pid = -1: 要求知道任何一个子进程的终止状态
  • pid = 0: 要求知道进程号为pid的子进程的终止状态
  • pid取值小于-1时,要求知道进程组号为pid的绝对值的子进程的终止状态

参数option让用户指定附加选项。最常用的选项是WHO_HANG,它通知内核在没有已终止子进程时不要阻塞

当前有终止的子进程时,返回值为子进程的ID号,同时参数statloc返回子进程的终止状态,否则返回值为-1

waitpid函数的用法如下:

pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WHOHANG)) > 0)
printf("child %d terminated\n", pid);

exit函数

本函数用来终止进程,返回状态

#include <stdlib.h>
void exit(int status);

本函数终止调用进程,关闭所有子进程打开的描述符,向父进程发送SIGCHLD信号,并返回状态,随后父进程就可通过调用wait或waitpid函数获得终止子进程的状态了

多进程并发服务器

多进程并发服务器建立过程

建立连接->父进程调用fork()函数产生子进程->父进程关闭已连接套接字;子进程关闭监听套接字->子进程处理客户请求,父进程等待另一个客户连接

下面用图例说明父进程调用fork生成子进程后,父、子进程对描述符的操作过程

当服务器调用accept函数时,连接请求从客户到达服务器时双方的状态如下图所示:

客户的连接请求被服务器接收后,新的已连接套接字即connfd被创建,可通过此描述符读、写数据,此时状态如下图所示:

服务器的下一步就是调用fork函数,如下图所示,给出了从fork函数返回后的状态,此时描述符listenfd和connfd在父、子进程间共享:

接下来就由父进程关闭已连接描述符(connfd),由子进程关闭监听描述符(listenfd),当前双方状态如下图所示:

到此就是套接字的最终状态子进程处理与客户的连接,父进程可以对监听描述符再次调用accept,继续处理下一个客户的连接请求