Linux下基于TCP的Socket编程
来源:互联网 发布:c语言数据类型范围 编辑:程序博客网 时间:2024/05/14 10:33
一直都觉得linux是一个很高大上的操作系统,对它充满兴趣,但是真心觉得它上手好难,因为是零基础开始学linux,连安装一个ubuntu,就安装了三天,总是出问题,但是又没人可以请教,就只能自己百度,有的时候明明一个懂的人提点一下,就可以解决的问题,所以我就想要不要报一个培训班,正好我认识一个学长他就是做嵌入式的,和他聊了一下,他推荐我去汇文,他说那里真的可以学到东西,我又自己咨询了一下老师,最后决定参加杭州汇文的嵌入式培训。在这里,我们从简单的c语言开始到网络编程再到开发板,一路走来,收获了许多。老师讲解的十分到位,从c语言到高级编程,让我一步一步走入linux的世界。在学习中,难免会遇到问题,自己死命都解决不了的问题,到了老师面前就是小case,前段时间,我们学习TCP/IP协议,因为我是自动化专业的学生,没接触过网络协议,理解起来比较吃力,老师就画图给我们讲解,每次下课,老师都先坐一下,等到没有同学问问题了,才会离开。下面的内容是我对老师讲解的基于TCP的socket编程的总结,希望可以帮助到大家。
网络字节序:
*TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
*为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例 如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
socket地址的数据类型:
*socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面 要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同,如下图所示:
struct sockaddr {
unsigned short sa_family; //16位地址类型
char sa_data[14]; //14字节地址数据
};
struct sockaddr_in {
short sin_family; //16位地址类型
unsigned short sin_port; //16位端口号
struct in_addr sin_addr; //32位IP地址
unsigned char sin_zero[8]; //8字节填充
};
*IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位 端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址 和一些控制字段。
*UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。
*IPv4、IPv6和UNIX Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:
truct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
*基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示 和in_addr表示之间转换。
字符串转in_addr的函数:
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);
in_addr转字符串的函数:
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。
基于TCP协议的客户端/服务器程序的一般流程:
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态;
客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后 从accept()返回。
DDOS攻击:SYN输入不存在的地址,服务器处理后,返回给源机器,因为根本就不存在这样的地址,所以服务器无法接到应答
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户 端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻 调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调 用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器 的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一 方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
服务器端:
#include <sys/socket.h>
int socket(int family, int type, int protocol); //打开一个网络通讯端口,全双工的管道
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示 面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的 传输协议。protocol参数的介绍从略,指定为0即可。
int bind(int sockfd, const struct sockaddr *myaddr,socklen_t addrlen); //绑定一个固定的网络地址和端口号
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址 和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端 口号。bind()成功返回0,失败返回-1。
bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的:
bzero(&servaddr, sizeof(servaddr)); //整个结构体清零
servaddr.sin_family = AF_INET; //设置地址类型为AF_INET,即IPv4
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设 置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址*/
servaddr.sin_port = htons(SERV_PORT); //端口号为SERV_PORT
int listen(int sockfd, int backlog); //声明sockfd处于监听状态
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); //等待客户端的连接,读取客户端的地址和端口号
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连 接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。如果给cliaddr参数传NULL,表示不关心客户端的地址。
addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。
我们的服务器程序结构是这样的:
while (1) {
cliaddr_len = sizeof(cliaddr);
cli_fd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if(cli_fd==-1)
{
perror("accept");
return 1;
}
if(cli_fd>=0) //连接成功
{
err=pthread_create(&id,NULL,thread_fun,(void*)cli_fd); //创建线程
if(err) //返回值非0,创建失败
{
printf("pthread_create fail with error %d\n",err);
return 1;
}
}
}
整个是一个while死循环,由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。当没有客户端请求连接时,程序阻塞在accept()函数这里,accept()的参数sockfd是先前的监听文件描述符, 而accept()的返回值是另外一个文件描述符cli_fd,之后与客户端之间就通过这个cli_fd通讯,我们的程序是当有客户端请求连接并且连接成功后,我们会创建一个线程,线程函数的最后的一个参数我们传递的是(void*)cli_fd,在线程中获取这个值,就可以在线程函数中进服务器与客户端的通信了,当断开连接时,要关闭cli_fd从而断开连接,而不关闭sockfd。
客户端:
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
int socket(int family, int type, int protocol); //打开一个网络通讯端口
sockfd = socket(AF_INET, SOCK_STREAM, 0);
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); //连接服务器
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
程序中对servaddr参数是这样初始化的:
bzero(&servaddr, sizeof(servaddr)); //整个结构体清零
servaddr.sin_family = AF_INET; //设置地址类型为AF_INET,即IPv4
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); //字符串转in_addr,
servaddr.sin_port = htons(SERV_PORT); //端口号为SERV_PORT
以上就是我对基于TCP的socket编程的总结,如果有什么不对的地方,希望大家多多包涵,很乐于得到大家的意见,共同学习,共同进步,谢谢。
- Linux下基于TCP的Socket编程
- 基于TCP协议下的socket编程
- 基于Linux下的TCP编程
- 基于Linux下的TCP编程
- 基于Linux下的TCP编程
- 基于Linux下的TCP编程
- 基于Linux下的TCP编程
- 基于Linux下的TCP编程
- 基于Linux下的TCP编程
- 基于Linux下的TCP编程
- linux下基于tcp的FTP编程
- 基于TCP的socket编程
- 基于TCP的socket编程
- 基于TCP的socket编程
- 基于TCP的socket编程
- 基于TCP的socket编程
- 基于TCP的Socket 编程
- 基于TCP的socket编程
- 最大报销额
- Installation error: INSTALL_FAILED_SHARED_USER_INCOMPATIBLE
- !HDU 4283 屌丝联谊会-区间dp
- js获取select标签选中的值
- OC语言类的深入和分类
- Linux下基于TCP的Socket编程
- 【剑指Offer面试题】 九度OJ1388:跳台阶
- Unicode和UTF-8之间的转换详解
- 安卓使用Socket发送中文,C语言服务端接收乱码问题解决方案
- poj 2251(BFS)
- mysql实现增量备份
- N!
- OC语言构造方法
- log4net 存储到oracle 调试 Could not load type [log4net.Appender.OracleAppender]