个性化阅读
专注于IT技术分析

Linux socket网络编程(一):TCP编程实例、socket编程函数、套接字类型和网络模型

一、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的二进制表示

IP地址有网络段和主机端两部分,主要根据这两部分位数的不同而分类,如C类地址的网络段有24位,主机段有8位,一般我们局域网内的私有地址都是C类地址。

IP网络段和主机段

网络层IP协议根据IP地址决定数据的传输方向,根据网络段判断源地址和目标地址是否在同一个网段,根据主机段找到确切的主机。一个IP头的详细信息如下:

IP协议数据包头信息

传输层的功能是决定数据的传输方式,主要的协议有TCP协议和UDP协议,TCP协议面向连接,需要提前确定连接的可靠性才建立TCP连接,UDP协议面向非连接,不需要确保连接的可靠性。

TCP/UDP协议的主要内容是:源端口(Source Port)和目标端口(Destination Port),源端口即本地应用的端口,目标端口为传输目的主机的端口。端口一般是应用层的应用程序提供,常见的有FTP文件服务21端口,HTTP服务默认80端口等,端口的作用是标明应用程序。一个TCP协议的详细报头信息如下:

TCP-UDP协议数据报头信息

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);
        }
}
赞(1)
未经允许不得转载:srcmini » Linux socket网络编程(一):TCP编程实例、socket编程函数、套接字类型和网络模型

评论 抢沙发

评论前必须登录!