第11章 网络编程

1.

1.1为什么要分层?

减少协议设计的复杂性。

1.2TCP/IP各层功能

2.套接字编程

通常Linux下的网络编程就是指套接字编程.

2.1 套接字地址结构

struct sockaddr

//linux/socket.h
struct sockaddr{
	unsigned short   sa_family;   //套接字的协议族类型,AF_XXX
	char             sa_data[14];//14字节,存储具体的协议地址
}
//长度16字节;一般在编程中不对该结构体进行操作,而是使用1个与它等价的数据结构:sockaddr_in

每种协议族都有自己的协议地址格式,TCP/IP协议族的地址格式为结构体struct sockaddr_in

/* netinet/in.h */
struct sockaddr_in{
  	unsigned short      sin_family; //地址类型,对于TCP/IP:该值为AF_INET
    unsigned short int  sin_port;   //端口号
    struct in_addr      sin_addr;   //32位IP地址
    unsigned char       sin_zero[8];//填充字节,一般赋值为0
}
//长度也为16字节
struct struct in_addr{
		unsigned long s_addr;
}

通常在编写基于TCP/IP协议的网络程序时,使用结构体sockaddr_in来设置地址,然后强制类型转换成sockaddr类型.

设置地址信息的示例

struct sockaddr_in struct sockaddr_in sock;
sock.sin_family = AF_INET;
sock.sin_port = htons(80); //设置端口号
sock.sin_addr.s_addr = inet_addr("202.205.3.195");
memset(sock.sin_zero,0,sizeof(sock.sin_zero));

memset(void *s,int c,size_t n); //它将s指向的内存的前n个字节赋值为c的值

2.2 创建套接字

​ 用来socket函数创建1个套接字,函数原型

#include #include 
#include 
int socket(int domain,int type,int protocol);

参数domain指定所使用的协议族,在linux/socket.h中定义. 常用的有

  • AF_UNIX : 创建只在本机内进行通信的套接字
  • AF_INET : 使用IPv4 TCP/IP协议
  • AF_INET6 : 使用IPv6 TCP/IP协议

参数type指定套接字的类型,可以取如下值:

  • SOCK_STREAM : 创建TCP流套接字
  • SOCK_DGRAM : 创建UDP数据报套接字
  • SOCK_RAW : 创建原始套接字

参数protocol通常设置为0. 当创建原始套接字时,需要设置该参数.

执行成功返回1个新创建的套接字;错误返回-1,错误代码存入errno中.

创建1个TCP套接字:

int sock_fd;
sock_fd = socket(AF_INET,SOCK_STREAM,0);
if(sock_fd < 0){
  	perror("socket error");
    exit(1);
}

创建1个UDP套接字:

sock_fd = socket(AF_INET,SOCK_DGRAM,sock_fd = socket(AF_INET,SOCK_DGRAM,0);

2.3 建立连接

函数connect用来在一个指定的套接字上创建1个连接,函数原型

#include #include 
#include 
int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen);

通常对于TCP套接字只能调用一次connect函数,而对于UDP套接字则可以调用多次以改变与目的地址的绑定。

将参数serv_addr中的sa_family设置为AF_UNSPEC可以取消绑定.

执行成功返回0;错误返回-1,错误代码存入errno中.

常见用法:

struct sockaddr_in       struct sockaddr_in       serv_addr;
memset(&serv_addr,0,sizeof(struct sockaddr_in));//将serv_addr各个字段清0
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(80); //设置端口号
//inet_aton将1个字符串转换成网络地址赋值给第二个参数
if(inet_aton("202.205.3.195",&serv_addr.sin_addr) < 0){
  perror("inet_aton");
  exit(1);
}
//使用sock_fd套接字连接到serv_addr指定的目的地址上,假定sock_fd已经定义
if(connect(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) < 0){
    perror("connect");
    exit(1);
}

2.4 绑定套接字

函数bind用来将1个套机子和某个端口绑定在一起,函数原型

#include #include 
#include 
int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen);

socket函数只是创建了1个套接字,没有指定工作在哪个端口. 服务端IP和端口号一般是固定的,该函数一般只有服务端的程序调用。

参数my_addr指定了sockfd将绑定到的本地地址.

执行成功返回0;错误返回-1,错误代码存入errno中.

常见用法:

struct sockaddr_in       struct sockaddr_in       serv_addr;
memset(&serv_addr,0,sizeof(struct sockaddr_in));//将serv_addr各个字段清0
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(80); //设置端口号
serv_addr.sin_addr.s_addr = hton1(INADDR_ANY);

if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) < 0){
  perror("bind");
  exit(1);
}

2.5 在套接字上监听

函数listen把套接字转化为被动监听

#include #include 
int listen(int sock_fd,int backlog);

函数socket创建的套接字是主动套接字,可以用来主动请求连接到某个服务器(通过函数connect).

但是作为服务端的程序,通常在某个端口监听等待来自客户端的连接请求.

在服务器端,一般是 socket -> bind -> listen 转化为监听套接字.

一般多个客户端连接到1个服务器。服务器设置一个连接队列,记录已建立的连接,参数backlog指定了该连接队列的最大长度。超过之后的连接请求将被拒绝.

执行成功返回0;错误返回-1,错误代码存入errno中.

常见用法:

##define LISTEN_NUM 12
...
if(listen(sock_fd,LISTEN_NUM) < 0){
  perror("listen");
  exit(1);
}

2.6 接受连接

函数accept用来接受一个连接请求,原型

#include #include 
#include 
int accept(int sock_fd,struct sockaddr *addr,socklen_t *addrlen);
  • 参数sock_fd是监听套接字

  • 参数addr用来保存发起连接请求的主机的地址和端口

  • 参数addrlen是addr所指向的结构体的大小

    执行成功返回1个新的代表1个新的套接字描述符,是客户端的套接字,;错误返回-1,错误代码存入errno中.

​ 只能对面向连接的套接字使用accept函数. 新的套接字描述符类似打开文件时返回的文件描述符,进程可以利用它与客户端交换数据,参数sock_fd所指定的套接字继续等待客户端的连接请求.

​ accept()可能会被阻塞.

2.7 TCP套接字的数据传输

1.发送数据

​ 函数send用来在TCP套接字上发送数据,原型

#include #include 
#include 
ssize_t send(int sock_fd, const void *buffer, size_t length, int flags);
  • 参数sock_fd为已建立好连接的,即accpet函数的返回值
  • buffer指向存放待发送数据的缓冲区
  • length为待发送数据的长度
  • 参数flags略

执行成功(只是说明数据写入套接字的缓冲区,不表示到达目的地)返回实际发送数据的字节数;错误返回-1,错误代码存入errno中.

套接字为阻塞方式下,常见用法:

##define BUFFERSIZE 1500
char send_buf[BUFFERSIZE];
...
if(send(conn_fd,send_buf,len,0) < 0){
   perror("send");
   exit(1);
}

2.接收数据

​ 函数recv用来在TCP套接字上发送数据,原型

#include #include 
#include 
ssize_t send(int sock_fd, void *buffer, size_t length, int flags);

​ 函数recv从参数sock_fd所指定的套机子描述符(必须是面向连接的)上接收数据并保存到参数buffer所指定的缓冲区,参数len则为缓冲区长度.

​ flags为控制选项,一般设置为0

执行成功返回接收到的数据的字节数;错误返回-1,错误代码存入errno中.

套接字为阻塞方式下,常见用法:

char  recv_buf[BUFFERSIZE];
...
if(recv(coon_fd,recv_buf,sizeof(recv_buf),0) < 0){
 	 perror("recv");
   exit(1);
}

2.8 UDP套接字的数据传输

2.9 关闭套接字

1. 函数close

用来关闭一个套接字描述符,与关闭文件描述符是类似的

#include #include 
int close(int fd)

执行成功返回0;错误返回-1,错误代码存入errno中.

2. 函数shutdown

也用于关闭一个套接字描述符,比close功能强大,允许单向关闭或全部禁止

#include #include 
int shutdown(int sock_d,int howto)

参数howto指定了关闭的方式

  • SHUT_RD : 将连接上的读通道关闭,此后进程将不能再接收任何数据,接收缓冲区未被读取的数据将被丢弃,但任然可以在该套接字上发送数据
  • SHUT_WR : 将连接上的写通道关闭,此后进程将不能再发送任何数据,发送缓冲区未被读取的数据将被丢弃,但任然可以在该套接字上接收数据
  • SHUT_RDWR : 读、写通道都将关闭

执行成功返回0;错误返回-1,错误代码存入errno中.


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!