socket编程
0.数据传输三要素
源 目的 长度
1.socket编程 基本概念
1.1TCP/IP
TCP/IP 协议叫做传输控制/网际协议,又叫网络通信协议。实际上,它包含了上百个功能的协议,如ICMP(互联网控制信息协议)、 FTP(文件传输协议)、UDP(用户数据报协议)、ARP(地址解析协议)等。 TCP 负责发现传输的问题,一旦有问题就会发出重传的信号,直到所有数据安全正确地传输到目的地。而 IP 就是给因特网的每一台电脑规定一个地址
1.2IP
IP地址的作用是标识计算机的网卡地址,每一台计算机都有唯一个 IP地址。在程序中是通过 IP 地址来访问一台计算机的。 IP 地址具有统一的格式, IP 地址的长度是 32 位的二进制数值, 4 个字节。我们为了便于记忆,通常化为十进制的整数来表示
1.3端口
所谓端口,是指计算机中为了表识同一计算机中不同程序访问网络而设置的编号。每个程序在访问网络时都会分配一个标识符,程序在访问网络或接受访问时,会用这个标识符表示这一网络数据属于这个程序。端口号其实是一个 16 位的无符号整数(unsigned short)也就是 0~65535。不同编号范围的端口号有不同的作用。低于 256 的端口是系统保留端口号,主要用于系统进程通信。如 WWW服务使用的是 80 号端口, FTP 服务使用的是 21 号端口。不在这一范围内的端口号是自由端口号,在编程时可以调用这些端口号
1.4域名
域名是用来代替 IP 地址来标识计算机的一种直观名称。如百度网址 IP 是119.75.213.50,没有任何逻辑含义,不便于记忆。我们一般选择 www.baidu.com这个域名来代替 IP 地址。
2. 套接字
套接字(socket),在网络中用来描述计算机中不同程序与其他计算机程序的通信方式。人们常说的 socket 其实是一种特殊的 IO 接口,它也是一种文件描述符
一、 流式 socket(SOCK_STREAM)流式套接字提供可靠的、面向连接的通信流;它使用 TCP 协议,从而保证了数据传输的正确性和顺序性。
二、 数据报 socket(SOCK_DGRAM)数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用的数据报协议是 UDP。
三、 原始 socket
原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发
2.1套接字定义
套接字(也叫套接口)由三个参数构成: IP 地址、端口号、传输层协议,以区分不同应用程序进程间的网络通信与连接。socket 也有一个类似于打开文件的函数调用,该函数返回一个整型的 socket描述符,随后的建立连接、数据传输等操作都是通过 socket 来实现的
2.2套接字数据结构
C 程序进行套接字编程时,常会使用到 sockaddr 数据类型和 sockaddr_in数据类型,用于保存套接字信息。
struct sockaddr
{
/*地址族,就是一些协议类型的集合*/
unsigned short sa_family;
/*14 字节的协议地址,包含该 socket 的 IP 地址和端口号*/
char sa_data[14];
};
struct sockaddr_in
{
short int sa_family; /*地址族*/
unsigned short int sin_port; /*端口号*/,查看/etc/services目录哪些端口号被使用了
struct in_addr sin_addr; /*IP 地址*/
/*全填 0,保持与 struct sockaddr 同样大小*/
unsigned char sin_zero[8];
};
上面这二个数据类型是等效的,可以相互转化, 但一般都使用 sockaddr_in这种形式。
sa_family 常见值如下:
AF_INET: IPv4 协议
AF_INET6: IPv6 协议
AF_LOCAL: UNIX 域协议
2.3 域名、主机名与 IP 地址转换
通常,人们在使用过程中都不愿意记忆一连串的 IP 地址, 因此,使用主机名将会是一个很好的选择。在 Linux 中,有一些函数可以实现主机名和地址间的转换。 其中gethostbyname 是将主机名转换为 IP 地址, 而 gethostbyaddr 是将IP 地址转换为主机名。 这两个函数都涉及一个 hostent 结构体
struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*地址类型*/
int h_length; /*地址长度*/
char **h_addr_list; /*指向 IPv4 或 IPv6 的地址指针数组*/
}
/* hent = gethostbyname(hname); */
hent = gethostbyname2(hname, AF_INET);
if (NULL == hent)
{
perror("gethostbyname failed");
fprintf(stderr, "host: %s\n", hname);
goto failure;
}
printf("hostname: %s\naddress list: ", hent->h_name);
for (i = 0; hent->h_addr_list[i]; i++)
{
printf("%s\t", inet_ntoa(*(struct in_addr *)(hent->h_addr_list[i])));//循环打印IP地址,一个主机可能有多个IP。
}
in_addr_t inet_addr(const char *cp)
这个函数将一个点分十进制的 IP 地址字符串转换成 in_addr_t 类型,该类型实际上是一个 32 位无符号整数,事实上就是前文提到的 struct in_addr 结构中的 s_addr 域的数据类型。 注意这个二进制表示的 IP 地址是网络字节序的。
这个函数其实在前文举例填充 struct sockaddr_in 的时候用过了。 192.168.0.1 在 PC 上会被转换成 0x0100A8C0
char *inet_ntoa(struct in_addr in)
此函数可以将结构 struct in_addr 中的二进制 IP 地址转换为一个点分十进制表示的字符串,返回这个字符串的首指针。使用起来很方便。但是要注意,它返回的缓冲区是静态分配的,在并发或者异步使用时要小心,因为缓冲区随时可能被其它调用改写。 如下调用,会将一个网络字节序二进制无符号 32 位整数表示的 IP 地址 0x0100A8C0 转换为点分十进制表示"192.168.0.1"
2.4数据存储优先级顺序
计算机数据存储分高字节优先和低字节优先。即大端、小端的问题。而Internet 上数据是以高位字节优先顺序在网络上传输的,但是 ARM 等一些 CPU,除了摩托罗拉公司的 CPU 是大端的,常见的 CPU 都是小端格式存储数据的。所以我们有必要对这两个字节存储优先顺序进行互相转化一些。这里有: htos、ntohs、 htonl、 ntohl 四个函数实现网络字节序和主机字节序的转化,这里的 h代表 host, n 代表 network, s 代表 short, l 代表 long。通常 16 位的 IP 端口号用 s 代表,而 IP 地址用 l 来代表。
uint16_t htons(uint16_t host16bit)
uint32_t htonl(uint32_t host32bit)
uint16_t ntohs(uint16_t net16bit)
uint32_t ntohs(uint32_t net32bit)
3.TCP概述
通常应用程序通过打开一个 socket 来使用 TCP 服务, TCP 管理到其他socket 的数据传递。可以说,通过 IP 源/目的可以唯一的区分网络中两个设备的关联,通过 socket 的源/目的可以唯一的区分网络中两个应用程序的关联
3.1三次握手
TCP 对话通过三次握手来初始化,三次握手的目的是使数据段的发送和接收同步;告诉其他主机其一次可接收的数据量,并建立虚连接。下面简单描述了三次握手的过程:
1、初始化主机通过一个同步标志置位的数据段发出会话请求;
2、接收主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发生的数据段的起始字节的顺序号、应答并带有将受到的下一个数据段的字节顺序号。
3、请求主机再回送一个数据段,并带有确认顺序号和确认号。TCP 实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报到达目的地后,接收方的 TCP 实体将回送一个数据报,其中包含有一个确认序号,它的意思是希望收到下一个数据报的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送发重发该数据报
3.2TCP 协议编程相关函数介绍
网络上绝大多数的通信服务采用服务器机制(client/server), TCP 提供的是一种可靠的、面向连接的服务
函数具体用法可以查看man 2 socket ,man 7 socket有高级用法
3.3TCP 编程实例
该实例分为服务器端(server)和客户端(client),其中服务器端首先建立起socket,接着绑定本地端口,接着建立与客户端的联系,并接收客户端发送的消息。 而客户端则建立 socket 之后,调用 connect 函数来与服务器端建立连接,连接上后,调用 send 函数发送数据到服务器端。
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define PORT 4321
#define BUFFER_SIZE 1024
int main(int argc,char *argv[])
{
struct sockaddr_in server_addr;
int sockfd,sendbytes;
char buf[BUFFER_SIZE];
struct hostent *host;
/* 指定输入参数为3个,否则出错 */
if(argc != 3)
{
printf("Usage: ./clinet IP address Text\n");
exit(1);
}
/* 地址解析函数 */
if((host = gethostbyname(argv[1]))==NULL)
{
perror("gethostbyname");
exit(1);
}
memset(buf,0,sizeof(buf));
sprintf(buf,"%s",argv[2]);
/* 建立socket连接,IPv4协议,字节流套接字 */
//AF_INET=PF_INET
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
printf("Socket id = %d\n",sockfd);
/* 初始化sockaddr_in结构体 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(server_addr.sin_zero),8);
/* 调用connect函数主动发起对服务器的连接 */
if((connect(sockfd,(struct sockaddr *)&server_addr,sizeof(struct sockaddr)))==-1)
{
perror("connect");
exit(1);
}
printf("Connect server success!\n");
/* 发送消息给服务器端 */
if((sendbytes = send(sockfd,buf,strlen(buf),0))==-1)
{
perror("send");
exit(1);
}
/* 关闭socket */
close(sockfd);
return 0;
}
服务端代码如下
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define PORT 4321
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
int main(void)
{
struct sockaddr_in server_addr,client_addr;
int sin_size,recvbytes;
int ser_fd,cli_fd;
char buf[BUFFER_SIZE];
/* 建立socket连接,IPv4协议,字节流套接字 */
if((ser_fd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
printf("Socket id = %d\n",ser_fd);
/* 初始化sockaddr_in结构体 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;//任意IP
bzero(&(server_addr.sin_zero),8);
/* 绑定函数bind ,强制转换避免警告,第三个参数可取server_addr长度。。服务端要显式绑定,客户端会自动绑定*/
if(bind(ser_fd,(struct sockaddr *)&server_addr,sizeof(struct sockaddr))==-1)
{
perror("bind");
exit(1);
}
printf("Bind success!\n");
/* 调用listen函数,进行监听 */
if(listen(ser_fd,MAX_QUE_CONN_NM)==-1)
{
perror("listen");
exit(1);
}
printf("Listening......\n");
/* 调用accept函数,等待客户端的连接 */
if((cli_fd = accept(ser_fd,(struct sockaddr *)&client_addr,&sin_size))==-1)
{
perror("accept");
exit(1);
}
printf("Have client ready for connecting\n");
/* 调用recv函数接收客户端的请求 */
memset(buf,0,sizeof(buf));
if((recvbytes = recv(cli_fd,buf,BUFFER_SIZE,0))==-1)
{
perror("recv");
exit(1);
}
/* 将收到的信息(客服端发来的信息)打印出来 */
printf("Received a message:%s\n",buf);
/* 关闭socket */
close(ser_fd);
return 0;
}
先打开中端运行服务端程序,只有客户端运行正确才会出现后面2行
./server
Socket id = 3
Bind success!
Listening......
Have client ready for connecting
Received a message:hello
再打开一个终端运行客户端程序,这里根据本机IP
./client 10.201.1.80 hello
Socket id = 3
Connect server success!
4.UDP概述
UDP 即用户数据报协议,它是一种无连接协议,因此不需要像 TCP 那样通过三次握手来建立一个连接。同时,一个 UDP 应用可以同时作为应用的客户或服务器方。由于 UDP 协议并不需要建立一个明确的连接,因此建立 UDP 应用要比建立 TCP 应用简单得多。UDP 比 TCP 更能更好地解决实时性问题,如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用 UDP协议
4.1UDP 协议编程相关函数介绍
所谓无连接的套接字通信,指的是使用 UDP 协议进行信息传输。使用这种协议进行通信时,两个计算机之前没有建立连接的过程。需要处理的内容只是把信息发送到另外一个计算机,这种通讯方式比较简单, 涉及函数也比较少
4.2UDP 编程实例
该实例同样分为服务器端(server)和客户端(client), 服务器端首先建立socket,接着绑定本地端口, 随后并没有 listen 监听客服端,也没有 accept 等待连接, 只是在死循环里,直接等待接收数据。 而客户端就更加简单,在建立socket 后,直接调用 sendto 发送数据到服务器。这样就省去了很多 TCP 必须的步骤,而 UDP 正因为不是面向连接的,所以显得简单方便。值得注意的是,UDP 并不是可靠的通信方式
服务端程序
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main(void)
{
struct sockaddr_in server_addr,client_addr;
int sockfd;
char buf[BUFFER_SIZE];
int ret,len;
/* 建立socket连接,IPv4协议,数据报套接字 */
if((sockfd = socket(AF_INET,SOCK_DGRAM,0))==-1)
{
perror("socket");
exit(1);
}
printf("Socket id = %d\n",sockfd);
/* 初始化sockaddr_in结构体 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bzero(&(server_addr.sin_zero),8);
len = sizeof(server_addr);
/* 绑定函数bind */
if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr))==-1)
{
perror("bind");
exit(1);
}
printf("Bind success!\n");
while(1)
{
/* 等待客户端发送过来的数据,一旦有数据就接收进来 */
if((ret = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&server_addr,&len))==-1)
{
perror("recvfrom");
exit(1);
}
buf[len] = '0';
printf("The message is %s\n",buf);
if(strncmp(buf,"stop",4)==0)
{
printf("Stop to run!\n");
break;
}
/* 关闭socket */
close(sockfd);
exit(0);
}
return 0;
}
客户端程序
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main(int argc,char *argv[])
{
struct sockaddr_in server_addr;
int sockfd;
char buf[BUFFER_SIZE];
int sendbytes;
/* 指定输入参数为2个,否则出错 */
if(argc != 2)
{
printf("Usage: ./clinet Text\n");
exit(1);
}
memset(buf,0,sizeof(buf));
sprintf(buf,"%s",argv[1]);
/* 初始化sockaddr_in结构体 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//server_addr.sin_addr.s_addr=inet_addr("192.168.1.12");特定IP
bzero(&(server_addr.sin_zero),8);
/* 建立socket连接,IPv4协议,数据报套接字 */
if((sockfd = socket(AF_INET,SOCK_DGRAM,0))==-1)
{
perror("socket");
exit(1);
}
printf("Socket id = %d\n",sockfd);
//绑定和connect不是必须的,系统隐式处理
/* 发送消息给服务器端,可用select函数,设定超时时间 */
if((sendbytes = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&server_addr,sizeof(server_addr)))==-1)
{
perror("sendto");
exit(1);
}
/* 关闭socket */
close(sockfd);
return 0;
}
服务端运行结果如下
./udps
Socket id = 3
Bind success!
The message is hello
客户端运行结果如下
./udpc hello
Socket id = 3
发现UDP编程简单了很多,也不需要IP地址