linux服务器开发(基本网络编程)

服务器端网络编程基础步骤

#include
#include
#include
#include
#include
#include
#include
#include

int main()
{
char buf[1024];

struct sockaddr_in servaddr,cliaddr;//定义服务器地址结构体,和客户端地址结构体
socklen_t len=sizeof(cliaddr);//得到客户端结构体的大小

int listenfd=socket(PF_INET,SOCK_STREAM,0);//创建套接字

int flag = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));//设置套接字




bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(atoi("8888"));//设置端口号
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//设置服务器地址,这里的宏表示包含回环地址,和网路ip地址;

bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//绑定套接字,也就是设置套接字的属性

listen(listenfd,5);//将套接字由主动发送状态变为被动接收状态;,并设置请求队列中允许的最大请求数,大多数系统默认为5

while(1){
int connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&len);//阻塞等待客户端的连接;直到请求队列中出现请求
															//获取可用于传输的套接字;
strcpy(buf,"hello ,I am server\n");
send(connfd,buf,1024,0);//通过connfd将缓冲区的发送数据到客户端

memset(buf,0,sizeof(buf));//清空缓冲区
recv(connfd,buf,1024,0);//
puts(buf);

close(connfd);		//关闭套接字
		}
close(listenfd);//关闭监听套接字
exit(0);
}



客户端网络编程基础步骤

#include
#include
#include
#include
#include
#include
#include
#include

int main()
{
char buf[1024];	//创建缓冲区

struct sockaddr_in servaddr;//定义服务器地址结构体

int sockfd=socket(PF_INET,SOCK_STREAM,0);//创建套接字

bzero(&servaddr,sizeof(servaddr));	//清空服务器结构体地址内容
servaddr.sin_family=AF_INET;		//设置协议族
servaddr.sin_port=htons(atoi("8888"));	//设置服务器端口号
servaddr.sin_addr.s_addr=inet_addr("192.168.0.120");//设置服务器地址

connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));//连接服务器

memset(buf,0,sizeof(buf));//清空缓冲区
recv(sockfd,buf,1024,0);//接收服务器发送的数据
puts(buf);

memset(buf,0,sizeof(buf));//清空缓冲区
strcpy(buf,"hello,I am client");//填充缓冲区数据
send(sockfd,buf,1024,0);//向服务器发送数据

close(sockfd);//关闭套接字

exit(0);
}




三次握手的过程

linux服务器开发(基本网络编程)_第1张图片

第一次握手客户端向服务器发带有SYN =1 ,seq=x(随机序列号) 的请求数据包
第二次握手:收到数据包之后服务器端向客户端发送一个带有SYN=1,ACK=1+x,seq=y的响应数据报包
第三次握手:客户端向服务器发送一个带有ACK=1+y,seq= x+1的响应数据报
至此三次握手建立连接成功;

首先要直到Linux内核协议栈为TCP连接管理使用两个队列:
半连接队列:用来保存SYN_SENT和SYN_RECV两个连接状态
全连接队列:用来保存ESTABLESHED状态的连接,也就是说全连接队列存放3次握手成功的连接;

而一般来说accept的作用就是阻塞等待全连接队列不为空,然后从全连接队列中取出全连接状态的套接字;当然accept阻塞或不阻塞,需要由套接字的属性决定。

而listen()第二个参数,实际上就是指定的全连接队列最大可以存放的未取出的全连接数量;如果如果accept()不去取出里面的全连接,那么当全连接队列中全连接的数量达到设置的最大值,将不能再放入全连接。

accept()可以将获取对端建立全连接的,IP,端口号,协议等信息;实际上accpet本身和三次握手没有任何关系,起到的作用就是取出全连接队列中的一个全连接,获取其属性罢了。

setsockopt()函数介绍

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

第一个参数:套接字

第二个参数:协议层SOL_SOCKET、IPPROTO_IP、IPPROTO_TCP

第三个参数:需设置的选项如:
SOL_SOCKET
SO_BROADCAST   允许发送广播数据    int
SO_DEBUG     允许调试         int
SO_DONTROUTE   不查找路由       int
SO_ERROR     获得套接字错误     int
SO_KEEPALIVE   保持连接        int
SO_LINGER     延迟关闭连接      struct linger
SO_OOBINLINE   带外数据放入正常数据流 int
SO_RCVBUF     接收缓冲区大小     int
SO_SNDBUF     发送缓冲区大小     int
SO_RCVLOWAT    接收缓冲区下限     int
SO_SNDLOWAT    发送缓冲区下限     int
SO_RCVTIMEO    接收超时        struct timeval
SO_SNDTIMEO    发送超时        struct timeval
SO_REUSERADDR  允许重用本地地址和端口 int
SO_TYPE      获得套接字类型     int
SO_BSDCOMPAT   与BSD系统兼容      int

IPPROTO_IP
IP_HDRINCL    在数据包中包含IP首部  int
IP_OPTINOS    IP首部选项       int
IP_TOS       服务类型
IP_TTL       生存时间        int

IPPRO_TCP
TCP_MAXSEG    TCP最大数据段的大小   int
TCP_NODELAY    不使用Nagle算法     int

第4个参数:用来设置第三参数的选项;传入类型为指针,指向存放选项值的缓冲区

第5个参数: 第4个参数地址中存放数据类型的大小

作用:设置套接字特性

使用场景一地址重用

level =SOL_SOCKET

optname =REUSERADDR

如果在已经处于 ESTABLISHED状态下的socket(一般由端口号和标志符区分)调用close(socket)(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socke可以如下设置

int reuse=1;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,&reuse,sizeof(int));

PS:当我们设置地址重用,需要在bin()之前设置

使用场景二,强制关闭套接字,不经历TIME_WAIT

level=SOL_SOCKET

optname=SO_REUSEADDR

如果要已经处于连接状态的soket在调用close(socket)后强制关闭,不经历TIME_WAIT的过程:

int reuse=0;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));

使用场景三,由于网络状况,收发无法预期进行,设置收发时限

level=SOL_S0CKET,

optname=SO_SNDTIMEO 和 SO_RCVTIMEO

// 发送时限

int nNetTimeout=1000; // 1秒
setsockopt(socket,SOL_S0CKET, SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));

// 接收时限

int nNetTimeout=1000; // 1秒
setsockopt(socket,SOL_S0CKET, SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));

使用场景四 设置scocket缓冲区的大小

在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节(异步),系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:

level=SOL_S0CKET

optname=SO_RCVBUF 和SO_SNDBUF

// 接收缓冲区

int nRecvBuf=32*1024; // 设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));

// 发送缓冲区

int nSendBuf=32*1024; // 设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));

注意:并不是说你设置的多大,系统就会设置多大,系统一般会将我们设置的缓冲区大小加倍,并且不得小于tcp的接收缓冲区和发送缓冲区设置的默认最小值。 注意:TCP有发送缓冲区和接收缓冲区,但是UDP因为是不可靠的,它没有确认重传机制,不保存应用程序数据的副本,所以是没有发送缓冲区的,但是UDP有接收缓冲区。

使用场景五,希望不经过socket缓冲区到系统缓冲区的拷贝而影响程序性能(发送)

level=SOL_S0CKET

optname=SO_SNDBUF

int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(int));

使用场景六,希望不经过系统缓冲区到socket缓冲区的拷贝而影响程序性能(接收)

level=SOL_S0CKET

optname=SO_RCVBUF

int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));

使用场景七 一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:

level=SOL_S0CKET

optname=SO_BROADCAST

int bBroadcast = 1;
setsockopt(s, SOL_SOCKET, SO_BROADCAST, (const char*)&bBroadcast, sizeof(int));

使用场景八 设置存活检测

level=SOL_S0CKET

optname=SO_KEEPALIVE

int opt = 1;
if (setsockopt (m_nSock, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(int)) == -1)
{
    return 0;
}

使用场景九 延迟接收 实际上就是当接收到第一个数据之后,才会创建连接。对于像http这类非交互式的服务器,这个很有意义,可以防御空连接攻击

level=SOL_TCP

optname=DEFER_ACCEPT

打开这个功能后,内核在val时间之内还没有收到数据,不会继续唤醒进程,而是直接丢弃连接。 从三次握手上讲,就是设置这个状态之后,就算完成了三次握手,服务器socket状态也不是ESTABLISHED,而依然是 SYN_RCVD,不会去接收数据。

int val = 5;
setsockopt(fd, SOL_TCP, TCP_DEFER_ACCEPT, &val, sizeof(val));

使用场景十 优美地关闭连接

优雅关闭:如果发送缓存中还有数据未发出则将其发出去,并且收到所有数据的ACK之后,发送FIN包,开始关闭过程。TCP连接线关闭一个方向,此时另外一个方向还是可以正常进行数据传输。

强制关闭:如果缓存中还有数据,则这些数据都将被丢弃,然后发送RST包,直接重置TCP连接。两边都关闭了,服务端处理完的信息没有正常传给客户端。

一、使用shudown优雅的关闭连接

1.1、close()函数:

#include
int close(int fd);
close()函数会使套接字的引用计数减1,若套接字的引用计数为0,则彻底关闭连接,并且会关闭TCP两个方向的数据流,既不能发送数据也不能接收数据。close函数不能关闭一个方向上的连接,而shutdown函数可以实现。如果服务端还有没有处理完的数据,不能正常发送给客户端。

1.2、shutdown函数

int shutdown(int sock, int howto);
参数howto的取值:
SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。

1.3、close()和shutdown()的区别

close会关闭连接,并释放所有连接对应的资源,而shutdown并不会释放掉套接字和所有资源。
close有引用计数,例如父子进程都打开了某个文件描述符,其中某个进程调用了close函数,会使close函数的引用计数减1,直到套接字的引用计数为0,才会真正的关闭连接。而shutdown函数可以无视引用计数,直接关闭连接。
close的引用计数的存在导致不一定会发出FIN结束报文,而shutdown一定会发出FIN报文。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
调用 close()关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来。
默认情况下,close()引用计数为0后会立即往网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

二使用使用setsockopt设置SO_LINGER实现优雅的关闭连接

2.1 LINGER

LINGER结构包含了指定套接字的信息,当调用了close()函数关闭该套接字时,LINGER结构中的信息指定了如何处理未发送的数据。

typedef struct linger {
 
 u_short l_onoff;
 
u_short l_linger;
 
} LINGER, *PLINGER, *LPLINGER;

linux服务器开发(基本网络编程)_第2张图片
取值方案:

设置 l_onoff为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close调用会立即返回给调用者,如果可能将会传输任何未发送的数据;

设置 l_onoff为非0,l_linger为0,当调用close的时候,TCP连接会立即断开.send buffer中未被发送的数据将被丢弃,并向对方发送一个RST信息.值得注意的是,由于这种方式,不是以4次握手方式结束TCP链接,所以,TCP连接将不会进入TIME_WAIT状态,这样会导致新建立的可能和就连接的数据造成混乱。这种关闭方式称为“强制”或“失效”关闭。

设置 l_onoff 为非0,l_linger为非0,在这种情况下,会使得close返回得到延迟。调用close去关闭socket的时候,内核将会延迟。也就是说,如果send buffer中还有数据尚未发送,该进程将会被休眠直到一下任何一种情况发生:
a. send buffer中的所有数据都被发送并且得到对方TCP的应答消息(这种应答并不是意味着对方应用程序已经接收到数据)
b.延迟时间消耗完。在延迟时间被消耗完之后,send buffer中的所有数据都将会被丢弃。
这种关闭称为“优雅的”关闭。

2.2 setsockopt

setsockopt()函数的作用是设置套接字的选项。

使用示例:

struct linger tmp = {1, 1};
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));

你可能感兴趣的:(linux服务器开发笔记,服务器,网络,linux)