引言:当我们需要进程间的通信时:用到的管道、消息队列、共享内存、信号量、信号等通讯方式时,这些方式主要基于Linux内核进行单机通信,这也体现了该通信方式的局限性。因此当我们需要多机通信时就需要用到网络编程。
在进程间通信时,我们通过进程的PID号为相对应的进程发消息,那么对于网络来说,为了寻找到在与自身毫无联系的主机中运行的进程时,这样的方法是行不通的,因此需要用到地址。这里的地址包括接入网络的主机的IP地址、以及在相对应主机上运行的服务器的端口号。同时,在建立了连接后信息需要交互,在交互时需要用到相统一的协议。
关于端口号:
一台拥有IP地址的主机可以提供许多服务,比如web服务、FTP服务、SMTP服务等等,这些服务完全可以通过一个IP地址来实现,那么主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。
实际上是通过“IP地址+端口号”来区分不同的服务的。端口只是提供了一种访问通道。服务器一般是通过知名端口号进行识别的。例如,对于每一个TCP\IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,一切皆 “socket”。
经过unix漫长的发展历程,前人将抽象的协议操作做成了简单的socket接口,让应用者能够实际简单的操作。开发者只需要配置相应的信息即可。
对于socket是一个类似文件的操作方法:打开文件、写入/读出数据、关闭文件。在此需要了解相对应的函数以及基本步骤:
一、socket编程的基本步骤:
1、创建套接字 2、为套接字添加信息 3、监听网络连接 4、监听到有客户端接入,接受接入 5、数据交互 6、关闭套接字,断开连接
二、基本函数
1、socket()函数
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
2、bind()函数(IP号端口号与相应的描述字赋值函数)
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:用于绑定IP地址与端口号到socketfd
参数说明:
addr:是一个指向包含本机IP地址与端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,这个地址结构根据地址创建socket时的地址协议族的不同而不同。
addrlen:通常使用sizeof(struct sockaddr)。
特别需要介绍的是关于struct sockaddr的数据结构:
struct sockaddr {
unsigned short sa_family; //协议族
char sa_data[14]; //IP+端口号
};
struct sockaddr_in {
__kernel_sa_family_t sin_family; // 协议族
__be16 sin_port; // 端口号
struct in_addr sin_addr; //IP地址结构体
unsigned char size_zero[8]; //为了使两个数据结构保持大小相同,而保留的空字节
};
struck sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式,二者长度一样,都是16个字节。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。一般情况下,需要把sockaddr_in结构强制转换成sockaddr结构再传入系统调用函数中。
字节序:
字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
常见的字节序:小端字节序 大端字节序
网络字节序 = 大端字节序
网络传输过程中,可能会因为字节序的不同导致信息损坏。为了使数据经网络发送后保持一致,因此在端口与IP地址信息的传输中,务必将信息进行格式转换:
/*IP地址转换API*/
int inet_aton(const char* straddr,struct in_addr *addrp);
把字符串形式的“192.168.1.123”转为网络能识别的格式
char* inet_ntoa(struct in_addr inaddr);
把网络格式的ip地址转为字符串形式
/*字节序转换API*/
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值uint32_t
htonl(uint32_t host32bitvalue); //返回网络字节序的值uint16_t
ntohs(uint16_t net16bitvalue); //返回主机字节序的值uint32_t
ntohl(uint32_t net32bitvalue); //返回主机字节序的值
h代表host,n代表net,s代表short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY,INADDR_ANY指定地址让操作系统自己获取
3、listen()函数
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
socket中TCP的三次握手建立连接详解:
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
- 客户端向服务器发送一个SYN J
- 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
- 客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
图1、socket中发送的TCP三次握手
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。
socket中TCP的四次握手释放连接详解:
上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:
图2、socket中发送的TCP四次握手
图示过程如下:
-
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
-
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
-
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
-
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
4、accept函数()
NAME
accept - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
5、connect()函数(用于客户端)
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
具体函数应用:
//服务器代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main()
{
int sockid;
if((sockid = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
putchar('\n');
exit(-1);
}
//socket
struct sockaddr_in so_addr;
so_addr.sin_family = AF_INET;
so_addr.sin_port = htons(8989);
inet_aton("192.168.1.21",&so_addr.sin_addr);
if((bind(sockid,(struct sockaddr*)&so_addr,sizeof(struct sockaddr_in))) == -1)
{
perror("bind");
putchar('\n');
exit(-1);
}
//bind
listen(sockid,10);
//listen
int c_addr = accept(sockid,NULL,NULL);
//accept
//read/write
printf("accept\n");
while(1);
return 0;
}
//客户端代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int sockid;
char readBuf[128];
struct sockaddr_in co_addr;
char *writeBuf = "Hello World";
int size = sizeof(struct sockaddr_in);
memset(&co_addr,0,sizeof(struct sockaddr_in));
if((sockid = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(-1);
}
co_addr.sin_family = AF_INET;
co_addr.sin_port = htons(8888);
inet_aton("192.168.1.21",&co_addr.sin_addr);
if(connect(sockid,(struct sockaddr *)&co_addr,sizeof(struct sockaddr_in)) == -1)
{
perror("connect");
exit(-1);
}
write(sockid,writeBuf,strlen(writeBuf));
int n_read = read(sockid,readBuf,128);
printf("recept %d byte:%s\n ",n_read,readBuf);
return 0;
}
关于TCP与UDP的一些区别:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丟失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流:UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一,和多对多的交互通信。
5.TCP首部开销20字节;UDP的首部开销小,只有8个字节
6. TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
以上仅为个人的学习总结,仅作个人学习回顾。部分内容来自csdn博主飞_哥,收益良多,感谢分享。