网络编程(一)——TCP编程基础

  • Post author:
  • Post category:其他



目录


1.基础知识


1.1 IP协议


1.1.1  IP地址的分类


1.1.2 子网掩码


1.1.3 网络字节序


1.2传输控制协议(TCP)


1.2.1 TCP传输的特点


1.2.2 TCP的数据格式


1.2.3 建立连接与断开连接


1.3.4 TCP的封装与解封过程


2.基本数据结构与接口


2.1 sockaddr和sockaddr_in


2.2 用户层与内核层的交互过程


2.2.1 向内核空间传入数据的交互过程


2.3 TCP网络编程流程


2.4 几个重要的函数详解


2.4.1 socket()


2.4.2 bind()


2.4.3 listen()


2.4.4 accept()


2.4.5 connect()


3. 一个简单的服务器/客户端的例子


1.基础知识

1.1 IP协议


版本(4位)

首部长度(4位)

服务类型(8位)

总长度(16位)

标识(16位)

标识(3位)

片偏移(13位)

生存时间TTL(8位)

协议类型(8位)

头部校验和(16位)

源IP地址(32位)

目的IP地址(32位)

选项(32位)

数据

  • 首部长度

    ——整个黄色区域的长度,以32位的字为单位。

  • 服务类型如下>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

字段

优先权

D

T

R

F

保留

长度(位)

3

1

1

1

1

1

含义


优先级


延迟

吞吐量

可靠性

费用

未用

服务类型字段由应用程序进行设置,路由器仅在必要的时候进行读取,不进行设置。


  • 总长度

    ——包括头部跟数据

  • TTL

    ——源主机在发送报文时设置TTL(一般为32或64),表示数据报文最多可以经过的路由器的数量。它指定报文的生存时间,每经过一个路由器TTL减1,当TTL为0的时候,路由器丢弃此包,并发送一个ICMP报文来通知源主机。

  • 协议类型如下>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>






协议类型



值2



协议类型3


1

ICMP

6

TCP

2

IGMP

17

UDP

  • IP选项

    ——用来标识是正常数据还是用来做网络控制的数据
  1. 安全与处理限制
  2. 路径记录:记录所经历路由器的IP地址
  3. 宽松源站路由:指定数据报文必须经历的IP地址,可以经过没有指定的IP地址
  4. 严格的源站路由:指定数据报文必须经历的IP地址,不能经过没有指定的IP地址

1.1.1  IP地址的分类

IP 地址可以分为A、B、C、D、E类

  • A类网络

网络标识占位

(1B)设为0

网络ID(7Bit)

支持127个网络

主机ID(24Bit)
  • B类网络

网络标识占位

(2B)设为10

网络ID(14Bit)

主机ID(16Bit)
  • C类网络

网络标识占位

(3B)设为110

网络ID(21Bit)

主机ID(8Bit)
  • D类网络(常用语组播)

网络标识占位

(4B)设为1110

网络ID(28Bit)
  • E类地址:保留,前四位为1111.

IP 32位都为0,表示主机本身。

1.1.2 子网掩码

子网掩码的主要作用:

  • 便于网络设备的尽快寻址,区分本网段地址和非本网段地址
  • 划分子网,进一步缩小子网的地址空间

1.1.3 网络字节序

网络字节序,默认为大端字节序。进行网络字节序转换的函数有以下几个

  • htons():对于short类型,从主机字节序转为网络字节序
  • ntohs():对于short类型,从网络字节序转为主机字节序
  • htonl(): 对于long类型,从主机字节序转为网络字节序
  • ntohl(): 对于long类型,从网络字节序转为主机字节序
#if ISLE
long htonl(long value)
{
    return ((value <<24) | ((value <<8)&0x00ff0000) |
	((value >> 8)&0x0000ff00) | (value >> 24));
}
#else if ISBE
long htonl(long value)
{
	return value;
}
#enif

1.2传输控制协议(TCP)

1.2.1 TCP传输的特点

  • 字节流服务
  • 面向连接服务
  • 可靠性传输
  • 缓冲传输
  • 全双工传输
  • 流量控制

1.2.2 TCP的数据格式

TCP数据在IP报文中的位置如下:

TCP报文的数据格式如下:


源端口号(16位)

目的端口号(16位)

序列号(32位)

确认号(32位)

头部长度(4位)

保留

(6位)

URG

ACK

PSH

RST

SYN

FIN

窗口尺寸(16位)

TCP校验和(16位)

紧急指针(16位)

选项(32位)

数据
  • 序列号——表示分配给TCP的编号。序列号用来标识应用程序从TCP的发送端到接收端发送的字节流。TCP开始连接——>发送一个序列号(seq)给接收端——>连接成功——>初始序列号(INS)=seq——>连接成功后第1、2…N个字节是INS+1、INS+2…INS+n(序列号是unsigned int,当达到最大值后从0开始).
  • 确认号——发送方对发送的首字节进行了编号,当接收方接收成功之后,发送回接收成功的序列号+1表示确认,发送方再发送的时候从确认号开始.
  • 控制位


列1



列2


字段

含义

URG

紧急指针字段

ACK

表示确认号有效

PSH

表示接收方应该迅速将此数据交给应用层

RST

重建连接

SYN

用于发起一个TCP连接

FIN

表示将要断开TCP连接
  • 窗口尺寸——表示本机上TCP协议可以接收的以字节为单位的数据
  • 校验和——校验TCP头部跟数据
  • 选项——TCP连接通常在第一个报文中指明这个选项,它指明当前主机所能接收的最大报文长度。

1.2.3 建立连接与断开连接

建立连接过程如下:

断开连接过程如下:

1.3.4 TCP的封装与解封过程

2.基本数据结构与接口

2.1 sockaddr和sockaddr_in

结构struct sockaddr和struct sockaddr_in的大小是完全一致的,所以进行地址结构设置时,通常的方法是利用struct sockaddr_in进行设置,然后强制转换为struct sockaddr类型。

2.2 用户层与内核层的交互过程

2.2.1 向内核空间传入数据的交互过程

向内核传入数据的函数有send()、bind()等,从内核得到数据的函数有acept()、recv()等。传入的过程入下图所示,bind()函数向内核传入的参数有套接字地址结构和结构的长度两个参数。参数addlen表示地址结构的长度,my_addr表示指向sockaddr的指针。调用bind(),地址结构通过内存复制的方式将其中的内容复制到内核,地址结构的长度通过传值的方式传入内核,内核按照用户传入的地址结构长度和地址结构的首地址来复制套接字的内容。

2.2.2 内核传出数据的交互过程

传出过程与传入过程不同的是,表示地址结构长度的参数在传入过程中是传值,而在传出过程中是通过传地址完成的。内核按照用户传入的地址结构长度进行套接字地址结构数据的复制,将内核中的地址结构数据复制到用户传入的地址结构指针中。

2.3 TCP网络编程流程

2.4 几个重要的函数详解

2.4.1 socket()

socket函数主要用于建立一个协议族为domin,协议类型为type、协议编号为protocol的套接字文件描述符,shibai

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domin, int type, int protocol)

应用层socket与内核函数之间的关系:

用户调用函数sock = socket(AF_INET,SOCK_STREAM,0)后会执行系统调用

sys_socket(AF_INET, SOCK_STREAM, 0) ,而sys_socket分为两部分,一部分生成内核socket结构,另一部分将socket结构与文件描述符绑定并将绑定后的fd传递给应用层。

2.4.2 bind()

bind()函数将长度为addrlen的struct sockadd类型的参数my_addr与sockfd绑定在一起,将sockfd绑定到某个端口上,如果使用connect()函数则没有绑定的必要。返回0表示成功,-1表示失败。

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen)

应用层bind()与内核函数之间的关系如下:

2.4.3 listen()

#include <sys/socket.h>
int listen(int sockfd, int backlog)
  • backlog:表示在accept()之前在等待队列中的客户端的长度,如果超过这个长度,客户端会返回一个ECONNREFUSED错误。

linsten()仅对SOCK_STREAM或SOCK_SEQPACKET有效,对其它类型将返回错误。运行成功返回0,失败-1.

应用层listen与内核之间的关系如下:

2.4.4 accept()

accept可以得到成功连接的客户端的IP地址、端口和协议等信息,这个信息是通过参数addr获取的,当accept返回的时候,会将客户端的信息存储在参数addr中。accept函数的返回值是新连接客户端套接字文件描述符,与客户端之间的沟通是通过这个描述符来操作的,而不是通过建立套接字时的文件描述符。

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

应用层accept与内核之间的关系如下:

2.4.5 connect()

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

应用层connect与内核之间的关系如下:

3. 一个简单的服务器/客户端的例子

<<<<<server.c>>>>>>>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 9999
#define BACKLOG 2

void process_conn_server(int s)
{
	ssize_t size = 0;
	char buffer[1024] = {0};
	for(;;){
		size = read(s, buffer, 1024);
		if(size == 0){
			return;
		}
		/*构建响应字符,为接收客户端字节的数量*/
		sprintf(buffer,"%d bytes altogether\n",size);
		write(s, buffer, strlen(buffer)+1);
	}
}

int main(int argc,char **argv)
{
	int ss,sc;
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	int err;
	pid_t pid;

	/*建立一个流式套接字*/
	ss = socket(AF_INET, SOCK_STREAM, 0);
	if(ss < 0){
		printf("socket error\n");
		return -1;
	}

	/*设置服务端地址*/
	bzero(&server_addr, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	/*本地地址*/
	server_addr.sin_port = htons(PORT);

	/*绑定套接字到套接字描述符*/
	err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
	if(err < 0){
		printf("bind error\n");
		return -1;
	}

	/*设置侦听*/
	err = listen(ss, BACKLOG);
	if(err < 0){
		printf("listen error\n");
		return -1;
	}

	for(;;){
		socklen_t addrlen = sizeof(struct sockaddr);
		sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);
		if(sc < 0){
			continue;
		}

		/*建立一个新的进程处理连接*/
		pid = fork();
		if(pid ==0){
			close(ss);
			process_conn_server(sc);
		}else{
			close(sc);
		}
	}
	
}

<<<<<client.c>>>>>>>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 9999

void process_conn_client(int s)
{
	ssize_t size = 0;
	char buffer[1024] = {0};
	
	for(;;){
		size = read(0, buffer, 1024);/*从标准输入读取数据到buffer*/
		if(size > 0){
			write(s,buffer,size);
			size = read(s,buffer,1024);
			write(1,buffer,size);
		}
	}
}

int main(int argc,char **argv)
{
	int s;
	struct sockaddr_in server_addr;
	
	s = socket(AF_INET, SOCK_STREAM, 0);
	if(s < 0){
		printf("socket error\n");
		return -1;
	}

	bzero(&server_addr, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	/*本地地址*/
	server_addr.sin_port = htons(PORT);

	/*将输入的字符串转为IP地址类型*/
	inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
	connect(s,(struct sockaddr*)&server_addr,sizeof(struct sockaddr));
	process_conn_client(s);
	
	close(s);
	return 0;
}

测试结果:



版权声明:本文为lixiangsheng2012原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。