一、OSI网络模型和TCP/UDP/IP协议
计算机网络编程必定少不了网络技术,目前标准的网络模型为ISO七层网络模型,分别为物理层、数据链路层、网络层、传输层、会话层、表现层、应用层,目前实现的是五层模型,更多可参考网络模型的原理和作用详细解释。这里侧重解析一下网络层和传输层,在网络模型中,每一层都有不同的协议,网络层的协议主要有IP互联网协议(Internet Protocol),该协议负责切分数据包,在IP协议中我们需要关注的有:源地址(Source address)和目标地址(Destination adress),这两个值就是我们常说的IP,源地址就是本机地址,目标地址是需要连接主机的地址。
就IP地址的类型来说,常见的有三类:A类地址,B类地址,C类地址,IPv4实质是用32位二进制表示,平时看到的都是十进制表示,如192.168.1.,34。
IP地址有网络段和主机端两部分,主要根据这两部分位数的不同而分类,如C类地址的网络段有24位,主机段有8位,一般我们局域网内的私有地址都是C类地址。
网络层IP协议根据IP地址决定数据的传输方向,根据网络段判断源地址和目标地址是否在同一个网段,根据主机段找到确切的主机。一个IP头的详细信息如下:
传输层的功能是决定数据的传输方式,主要的协议有TCP协议和UDP协议,TCP协议面向连接,需要提前确定连接的可靠性才建立TCP连接,UDP协议面向非连接,不需要确保连接的可靠性。
TCP/UDP协议的主要内容是:源端口(Source Port)和目标端口(Destination Port),源端口即本地应用的端口,目标端口为传输目的主机的端口。端口一般是应用层的应用程序提供,常见的有FTP文件服务21端口,HTTP服务默认80端口等,端口的作用是标明应用程序。一个TCP协议的详细报头信息如下:
UDP报头类似,但是信息更加简化。
所以,网络模型、TCP/UDP/IP协议和Socket网络编程有什么关系呢?我们使用socket开发的程序都属于应用层,网络编程至少需要提供网络层和传输层需要的信息,从上面你可以看到,在网络层需要提供IP协议中的本机和目标主机的IP地址,在传输层需要提供本机和目标主机的端口。顺便提一下,一般来说端口是应用程序提供的,但是由于有NAT的存在,如果两台主机不在同一个网段,被访问的主机的端口就不是应用程序的端口,而是NAT映射后的端口,IP也是映射后的IP地址。
二、socket套接字和相关信息的数据结构
套接字即socket,Linux网络编程主要使用socket接口,网络编程的实质是实现进程间的通信,只不过多数情况下,两个进程不在同一台主机或者同一个网段。套接字是一个数据结构,主要由网络层和传输层的相关信息组成。网络编程实际使用是一种通信资源,一种文件,但是我们在socket网络编程中并没有真正持有通信对象,没有socket,使用的是socket文件描述符,一个int整形数,这多少会令人费解,建议了解一下关于文件和文件描述符的原理分析。
1、网络层协议类型和socket连接类型
编程中主要使用socket描述符进行网络通信,创建socket首先需要确定网络层和传输层的协议类型,具体如下:
网络层常见协议类型(地址类型)
AF_LOCAL:进程通信协议;
AF_INET:IPv4网络协议;
AF_INET6:IPv6网络协议;
AF_IPX:IPX-Novell协议。
传输层的socket连接类型
SOCK_STREAM:提供TCP可靠的数据传输;
OOB机制:数据发送前必须使用connect()建立连接状态;
SOCK_DGRAM:提供不可靠的数据传输,即UDP用户数据报传输;
SOCK_RAW:原始网络协议存取。
2、socket信息数据结构
使用一个结构体封装socket连接必要的信息,主要使用的结构体有:
struct sockaddr{
unsigned short sa_family; // 地址协议类型
char sa_data[14]; // 14字节协议地址,包括IP地址和端口号
}
struct sockaddr_in{
short int sin_family; // 地址协议类型
unsigned short int sin_port; // 端口号
struct in_addr sin_addr; // IP地址结构体
unsigned char sin_zero[8]; // 填充0以兼容使用struct sockaddr_in*0
}
struct in_addr{
unsigned long s_addr; // IP地址
}
编程中我们使用的是struct sockaddr_in,但是函数接口是struct sockaddr,所以我们需要使用使用bzero或memset将结构体清零,但是这样编译还是有警告信息,处理警告信息可以使用类型强制转换。
3、端口和IP处理
计算机和网络存储数据的排列方式是不同的,上面我们看到的结构体struct sockaddr_in中的数据都会被封装到数据包中传输到网络,所以需要将该结构体中数据的字节顺序进行转换,但是sin_family不会传输到网络,该属性只是表明使用的地址协议类型,我们只需处理sin_port和sin_addr。
(1)首先是处理端口。本人对C的变量命名都是很困惑,有些变量缩短意思都好难猜,这里涉及要处理的函数,主要就是主机和网络的字节顺序转换,主机为s:host,网络为n:network,是to,另外有两种转换的数据类型,s:short,l:long。
端口处理一共有4个函数:htons(),htonl(),ntohs(),ntohl()。
(2)接着处理32位二进制IP地址。从上面我们可以看到IP地址s_addr是长整型的,但是我们一般使用的都是字符串形式的点分十进制,这是32位二进制和16位点分十进制之间的转换。
32位二进制转点分十进制IP地址:inet_ntoa()
点分十进制转32位二进制IP地址:inet_aton()、inet_addr();
兼容IPv4和IPv6的函数有:inet_pton()、inet_ntop()。
(3)域名和IP地址之间的转换。
为了方便我们通常会使用域名,但是实际网络数据传输使用的是IP地址,这里也需要进行转换,以下函数头文件为netdb.h
域名转IP地址:gethostbyname();
IP地址转为域名或主机名:gethostbyaddr()。
这两个函数的返回值是一个结构体,声明如下:
struct hostent{
char *h_name; //主机名
char **h_aliases; // 主机别名
int h_addrtype; // 主机IP地址协议类型
int h_length; // 主机IP地址字节长度
char **h_addr_list; // 主机IP地址列表
}
三、socket网络编程函数详解
1、socket()函数
函数定义:int socket(int domain,
int type, int protocol);
domain为地址协议类型,AF_INET为IPv4,AF_INET6为IPv6;
type为socket传输类型,SOCK_STREAM为TCP,SOCK_DGRAM为UDP传输;
protocol为传输协议的编号,一般为0。
该函数创建一个socket通信,返回socket文件描述符,失败返回-1。
2、bind()函数
函数定义:int bind(int sockfd,
struct sockaddr *addr, int addr_len);
sockfd为socket文件描述符;
addr为socket信息结构体,一般使用struct
sockaddr_in*;
addr_len为socket信息结构体的长度。
该函数将socket通信sockfd和socket通信信息addr绑定起来,失败返回-1。
3、listen()函数
函数定义:int listen(int s, int
backlog);
s为socket文件描述符;
backlog指同时能连接的最大请求。
该函数用于监听端口,等待连接。
4、accept()函数
函数定义:accept(int s, struct
sockaddr *addr, int *addrlen);
s为socket文件描述符;
addr为socket信息结构体;
addrlen为addr的长度。
该函数用于接受socket连接,成功返回新的socket,可使用该socket进行后续处理。
5、connet()函数
函数定义:int connet(int sockfd,
struct sockaddr *serv_addr, int addrlen);
sockfd为socket文件描述符;
serv_addr为服务器的连接信息结构体;
addrlen为serv_addr的长度。
该函数用于建立socket连接。
6、send()函数
函数定义:int send(int s, const
void *msg, int len, unsigned int flags);
s为socket文件描述符;
msg为需要发送的信息;
len为信息的长度;
flags一般设置为0即可。
该函数用户发送数据。
7、recv()函数
函数定义:int recv(int s, void
*msg, int len, unsigned int flags);
s为socket文件描述符;
msg为需要接收的信息;
len为信息的长度;
flags一般设置为0即可。
该函数用于接收数据。
四、socket编程实例
TCP服务端创建连接顺序:socket,bind,listen,accept,recv,send,close。
客户端的连接顺序:socket,connect,send,recv,close。
TCP服务端实例代码,单长连接,下节再处理多客户端的长连接:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]){
int socket_s = socket(AF_INET, SOCK_STREAM, 0);
if(socket_s < 0){
perror("server socket init");
return;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8989);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int addr_len = sizeof(struct sockaddr_in);
if(bind(socket_s, (struct sockaddr *)&addr, addr_len)){
perror("server bind");
return;
}
printf("start listening...\n");
if(listen(socket_s, 4) < 0){
perror("server listen");
return;
}
printf("end listen...\n");
int socket_re = accept(socket_s, (struct sockaddr *)&addr, &addr_len);
char buffer[256];
int i;
while(1){
struct sockaddr_in client_addr;
int caddr_len = sizeof(struct sockaddr_in);
printf("start accepting...\n");
// int socket_re = accept(socket_s, (struct sockaddr *)&client_addr, &caddr_len);
memset(buffer, 0, sizeof(buffer));
printf("start recving...\n");
recv(socket_re, &buffer, sizeof(buffer), 0);
int seconds = time(NULL);
printf("%d message\t%s\n", seconds, buffer);
char response[256] = "server response";
send(socket_re, &response, sizeof(response), 0);
// close(socket_re);
}
close(socket_s);
return 0;
}
客户端实例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
int main(int argc, char *argv){
int socket_s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
bzero(&addr, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(8989);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(socket_s, (struct sockaddr *)&addr, sizeof(addr));
char buffer[256];
char message[256];
while(1){
memset(buffer, 0, sizeof(buffer));
memset(message, 0, sizeof(message));
read(0, message, sizeof(message));
send(socket_s, &message, sizeof(message), 0);
recv(socket_s, buffer, sizeof(buffer), 0);
int seconds = time(NULL);
printf("%d message\t%s\n", seconds, buffer);
}
}