网络socket通信

网络层的"ip地址"可以唯一标识网络中的主机,而传输层的"端口",可以唯一标示主机中的应用程序(进程)。这样利用二元组(ip地址,端口)就可以标识网络中的进程了,网络中的进程通信就可以利用这个标志与其他的进程进行交互。使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。就目前而言,居户所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在。

TCP/IP协议族包括运输层、网路层、链路层、而socket所在的位置如图,Socket是应用与TCP/IP协议族族通信的中间软件抽象层。

Sokcet起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”。在许多操作系统中,套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后应用程序以描述符作为传递参数,通过调用相应函数(如read,write,close等)来完成某种操作(如从套接字中读取或写入数据)。

在生活中,A要电话给B,A拨号,B听到电话铃声后提起电话,这时A和B就建立起了连接,A和B就可以讲话了。等交流结束,挂断电话结束此次交谈。 打电话很简单解释了这工作原理:“open—write/read—close”模式。下面是网络socket通信的基 本流程

socket操作API函数

既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。
socket()函数
int socket(int domain, int type, int protocol);

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述符,而socket()用于创建一个socket描述符(socket descriptior),它唯一标示一个socket。这个socket描述字跟文件描述符一样,后续的操作都有用到,把它作为参数,通过他来进行一些读写操作。创建socket的时候,也可以制定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

        domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX, Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了 要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
        
        type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、 SOCK_SEQPACKET等等。
        
        protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、 IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默 认协议。
bind()函数
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数。通常服务器在启动的时候都会绑定一个众所周知的地址(如 ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身 的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生 成一个。当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addrlen:对应的是地址的长度。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地
址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核:
通用套接字 sockaddr 类型定义:
typedef unsigned short int sa_family_t;
struct sockaddr { 
 sa_family_t sa_family; /* 2 bytes address family, AF_xxx */
 char sa_data[14]; /* 14 bytes of protocol address */
}
ipv4对应的是sockaddr_in类型定义:
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in_addr {
 uint32_t s_addr; 
};
struct sockaddr_in {
 sa_family_t sin_family; /* 2 bytes address family, AF_xxx such as AF_INET */
 in_port_t sin_port; /* 2 bytes port*/
 struct in_addr sin_addr; /* 4 bytes IPv4 address*/
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[8]; /* 8 bytes unused padding data, always set be zero */
};
ipv6对应的sockaddr_in6类型定义:
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in6_addr
{
 union
 {
 uint8_t __u6_addr8[16];
 uint16_t __u6_addr16[8];
 uint32_t __u6_addr32[4];
 } __in6_u;
}
struct sockaddr_in6 {
 sa_family_t sin6_family; /*2B*/
 in_port_t sin6_port; /*2B*/
 uint32_t sin6_flowinfo; /*4B*/
 struct in6_addr sin6_addr; /*16B*/
 uint32_t sin6_scope_id; /*4B*/
};
Unix域对应的sockaddr_un类型定义:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
 sa_family_t sun_family; 
 char sun_path[UNIX_PATH_MAX]; 
};

各种结构体定义

从上面各种协议的结构体可以看出,通用套接字地址结构体(struct sockaddr)的大小是16个字节,IPv4套接字地址结构体(struct sockaddr_in)的大小也是16个字节。对于struct sockaddr_in和struct sockaddr它们开头一样,内存大小也一样,直接强制类型转换就可以取值使用了

网络字节序和主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,
这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节 序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
        所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于 这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以 请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再 赋给socket。参考之前服务器端用来设置监听的IP和端口的
代码:
serv_addr.sin_port = htons(LISTEN_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
这里通过调用两个函数 htons() 和 htolnl() 分别用来将 端口和IP地址转换成网络字节序,这两个函数名中的 h表示host, n表示network, s表示short(2字节/16位), l表示long(4字节/32位)。因为端口号是16位的,所以我们用htons()把端口号从主机字节 序转换成网络字节序, 而IP地址是32位的,所以我们用htonl()函数把IP地址从主机字节序转换成网络字节序。INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均 定义成为0值。这里也就意味着监听所有的IP地址。
listen函数
socket ()函数创建的socket默认是一个主动类型的,如果作为一个服务器,在调用socket(),bind()之后就会调用listen()来监听这个socket,该函数socket变为主动类型的,等待客户的连接请求。
int listen(int sockfd, int backlog);

sockfd:socket()系统调用创建的要监听的socket描述字体                                                                   backlog:相应的socket可以在内核里排队的最大连接个数

backlog说明:
TCP建立连接是要进行三次握手,但是否完成三次握手后,服务器需要维护这种状态:
  半连接状态为:服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态 为:SYN_RCVD)。
  全连接状态为:TCP的连接状态从服务器(SYN+ACK)响应客户端后,到客户端的ACK报文到达服务器之前,则一直保留在半连接状态中;
backlog其实是一个连接队列,在Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队列大小,当服务器接收到客户端的ACK报文后,该条目将从半连接队列搬到全连接队列尾部,即 accept queue (服务器端口状态为:
ESTABLISHED)。在Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接(ESTABLISHED状态)队列大小。
  SYN queue 队列长度默认为2048。
  Accept queue 队列长度取默认值或使用listen函数时传入的参数,二者取最小值。默认为128
accept()函数
        TCP服务器依次socket(),bind(),listen()之后,就会监听指定的socket地址了。服务器之后就调用accept()接受来自客户端的的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器程序,该程序将一直阻塞着不会返回,直到有一个客户顿啊连过来为止。一旦客户端调用connect()函数就会触发服务器的accept()返回,这时整个TCP连接就建立好了。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
*addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等;
addrlen: 返回客户端协议地址的长度 ;
accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个 服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户
连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
connect()函数
TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服务器。如果客户端这时调用connect()发 出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接, 通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 客户端的socket()创建的描述字
addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息
addrlen: socket地址的长度
在调用connect之前,我们需要先设置服务器端的IP地址和端口等信息到addr中去。参考下面客户端代码。
新型网路地址转化函数inet_pton和inet_ntop
在此前的代码中我们使用的inet_ato()或inet_ntoa()函数完成IPv4点分十进制字符串和32位整形数据之间的互相转换,但这两个函数只适合于IPv4的地址。下面这两个函数可以同时兼容IPv4和IPv6的地址:
//将点分十进制的ip地址转化为用于网络传输的数值格式,返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
int inet_pton(int family, const char *strptr, void *addrptr); 
 
//将数值格式转化为点分十进制的ip地址格式,返回值:若成功则为指向结构的指针,若出错则为NULL
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
read()、write()等函数
客户端在connect()连接服务器,并且服务器通过accept()建立起这个TCP socket链接之后,就可以调用网络I/O函数进行读写操作了,即实现了网络通信。网络I/O操作函数有下面几组:
 ssize_t read(int fd, void *buf, size_t count);
 ssize_t write(int fd, const void *buf, size_t count);
 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,
socklen_t addrlen);
 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t
*addrlen);
 ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
 ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
是网络socke fd也就意味着TCP 链接断开了;小于0表示出现了错误并设置错误标志到errno全局变量中,如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题;
write()将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。
1. write的返回值大于0,表示写了部分或者全部的数据。
2. 返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。
close()、shutdown()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文
件要调用close()来关闭一样。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述
字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
int close(int fd);
如果对socket fd调用close()则会触发该TCP连接断开的四路握手,有些时候我们需要数据发送出去并到达对方之后才能关闭socket套接字,则可以调用shutdown()函数来半关闭套接字:
int shutdown(int sockfd, int how);
如果how的值为 SHUT_RD 则该套接字不可再读入数据了; 如果how的值为 SHUT_WR 则该套接字不可再发送数据了; 如果how的值为 SHUT_RDWR 则该套接字既不可以读,也不可以写数据了。

socket服务器代码和客户端代码示例

服务器代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define LISTEN_PORT 8889
#define BACKLOG 13
int main(int argc, char **argv)
{
 int rv = -1;
 int listen_fd, client_fd = -1;
 struct sockaddr_in serv_addr;
 struct sockaddr_in cli_addr;
 socklen_t cliaddr_len;
 char buf[1024];

 listen_fd = socket(AF_INET, SOCK_STREAM, 0);
 if(listen_fd < 0 )
 {
 printf("create socket failure: %s\n", strerror(errno));
 return -1;
 }
 printf("socket create fd[%d]\n", listen_fd);

 memset(&serv_addr, 0, sizeof(serv_addr));
 serv_addr.sin_family = AF_INET;
 serv_addr.sin_port = htons(LISTEN_PORT);
 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
 if( bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0 )
 {
 printf("create socket failure: %s\n", strerror(errno));
 return -2;
 }
 printf("socket[%d] bind on port[%d] for all IP address ok\n", listen_fd, LISTEN_PORT);

 listen(listen_fd, BACKLOG);

 while(1)
 {
 printf("\nStart waiting and accept new client connect...\n", listen_fd);
 client_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cliaddr_len);
 if(client_fd < 0)
 {
 printf("accept new socket failure: %s\n", strerror(errno));
 return -2;
 }
 printf("Accept new client[%s:%d] with fd [%d]\n", inet_ntoa(cli_addr.sin_addr),
ntohs(cli_addr.sin_port), client_fd);


 memset(buf, 0, sizeof(buf));
 if( (rv=read(client_fd, buf, sizeof(buf))) < 0)
 {
 printf("Read data from client socket[%d] failure: %s\n", client_fd, strerror(errno));
 close(client_fd);
 continue;
 }
 else if( rv == 0 )
 {
 printf("client socket[%d] disconnected\n", client_fd);
 close(client_fd);
 continue;
 }
 printf("read %d bytes data from client[%d] and echo it back: '%s'\n", rv, client_fd, buf);

 if( write(client_fd, buf, rv) < 0 )
 {
 printf("Write %d bytes data back to client[%d] failure: %s\n", rv, client_fd,
strerror(errno));
 close(client_fd);
 }

 sleep(1);
 close(client_fd);
 }
 close(listen_fd);
 }

客户端代码

 #include <stdio.h>
 #include <errno.h>
 #include <string.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>

 #define SERVER_IP "127.0.0.1"
 #define SERVER_PORT 8889
 #define MSG_STR "Hello, Unix Network Program World!"

 int main(int argc, char **argv)
 {
 int conn_fd = -1;
 int rv = -1;
 char buf[1024];
 struct sockaddr_in serv_addr;
 conn_fd = socket(AF_INET, SOCK_STREAM, 0);
 if(conn_fd < 0)
 {
 printf("create socket failure: %s\n", strerror(errno));
 return -1;
 }

 memset(&serv_addr, 0, sizeof(serv_addr));
 serv_addr.sin_family = AF_INET;
 serv_addr.sin_port = htons(SERVER_PORT);
 inet_aton( SERVER_IP, &serv_addr.sin_addr );

 if( connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
 {
 printf("connect to server [%s:%d] failure: %s\n", SERVER_IP, SERVER_PORT, strerror(errno));
 return 0;
 }

 if( write(conn_fd, MSG_STR, strlen(MSG_STR)) < 0 )
 {
 printf("Write data to server [%s:%d] failure: %s\n", SERVER_IP, SERVER_PORT, strerror(errno));
 goto cleanup;
 }

 memset(buf, 0, sizeof(buf));
 rv = read(conn_fd, buf, sizeof(buf));
 if(rv < 0)
 {
 printf("Read data from server failure: %s\n", strerror(errno));
 goto cleanup;
 }
 else if( 0 == rv )
 {
 printf("Client connect to server get disconnected\n");
 goto cleanup;
 }
 printf("Read %d bytes data from server: '%s'\n", rv, buf);


 cleanup:
 close(conn_fd);
 }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值