tcp网络编程客户端和服务端及listen和tcp允许最大连接数

tcp网络编程
tcp网络编程步骤:
由于tcp传输特点是可靠有连接,那么就有
1.客户端向服务端发送连接请求(SYN),
2.服务端接受请求并向客户端发送(SYN+ACK);
3.客户端向服务端回复ACK表明他知道服务端同意连接。
以上三个步骤就是三次握手。
服务端编程步骤:
1.创建套接字
2.为套接字绑定地址信息
3.监听:开始接受服务端的连接请求
4.获取连接建立成功的新socket
5.发送数据
6.接受数据
1.创建套接字

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址域
		 AF_INET :ipv4协议
type: 套接字类型
		SOCK_STREAM 流式套接字
		SOCK_DGRAM  数据报套接字
protocol :协议类型 
		如果是0,则表示默认;流式套接字默认tcp协议,报式套接字默认udp协议
		流式套接字: IPPROTO_TCP 6 
		报式套接字:IPPROTO_UDP 17  
如:socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
返回值:成功:套接字描述符 
	   失败:-1						

2.为socket绑定地址信息

 #include <sys/socket.h>
 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 参数: sockfd: socket描述符
 	   addr :socket绑定的地址
 	   addrlen :地址信息长度
 返回值:成功:0(网卡操作那个进程),失败 -1
 功能:将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
 sockaddr结构:
 struct sockaddr {
               sa_f  amily_t sa_family;
               char        sa_data[14];
                }
虽然bind里参数是sockaddr,但是真正在基于IPV4编程时,使用的结构体是sockaddr_in;这个结构体里主要有三部分信息:地址类型,端口号,IP地址。
sockaddr_in在头文件#include<netinet/in.h>或#include<arpa/inet.h>中定义。该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下: 
structsockaddr_in{

short           sin_family;//AF_INET(地址族)PF_INET(协议族)

unsigned short  sin_port;/*Portnumber(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/

struct in_addr  sin_addr;//32位IP地址

unsigned char   sin_zero[8];//没有实际意义,只是为了跟SOCKADDR结构在内存中对齐*/

};
该结构体中提到的另一个结构体in_addr定义如下,它用来存放32位IP地址:
typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};   
in_addr用来表示一个IPV4的IP地址,其实是一个32位整数。   

客户端不推荐手动绑定地址信息 ,因为绑定有可能因为特殊原因失败,但是客户端具体使用哪个地址和端口都可以,只要能把数据发送出去,所以客户端程序不手动绑定地址,直至发送数据时,操作系统检测到socket没有绑定地址,会自动选择合适的地址和端口为socket绑定地址,这种数据一般不会出错。
3.监听(服务端监听后才可以接受客户端连接请求)

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);

listen()声明sockfd处于监听状态,并且最多允许backlog个客户端处于连接等待状态,如果接受到更多的连接请求就忽略,一般是5,即代表最大同时并发连接数为5,这个数字并不是tcp最大建立连接数。(文章后面会讲述tcp最大建立连接数)
返回值:成功: 0  失败 -1

4.accept():获取新建立的socket

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd : socket描述符
addr  :新建立连接的客户端地址信息
addrlen :地址信息长度
返回值:成功:返回新的socket连接描述符
	   失败:-1
accept是阻塞型函数,如果连接成功的队列没有新的连接,将会一直阻塞等待新的客户端连接

参数sockfd和返回值newsockfd区别:
sockfd :所有连接请求的数据发送到socket这个缓冲区(包括服务端ip和port),然后进行处理(为这个新建立连接的客户端新建立一个socket);
newsockfd: 连接建立成功后,连接成功的客户端发送的数据都发送到这个新的socket缓冲区(包括服务端ip port和建立连接客户端ip port)。
5.发送数据

 #include <sys/types.h>
 #include <sys/socket.h>
 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 flag :  0默认阻塞发送数据

由于accept返回的socket描述符中有客户端ip和port,所以参数中就没有struct sockaddr_in 和 addrlen,这是和udp发送数据的区别。同理,tcp和udp接受数据函数参数不同。

6.接受数据

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd: 里面已经包含从哪儿接受数据信息,是新的sockfd
buf:用于接受数据
len :用于接受数据长度
flags:  0  默认 阻塞式接收 
返回值 : 错误 : -1
        连接关闭 ; 0
        实际接受数据 >0

7.关闭socket描述符

要在任意可能退出的地方关闭对应的socket描述符。
tcp服务端代码

// tcp 服务端代码
//1.创建套接字
//2.绑定地址信息
//3.监听:监听之后获取新的socket连接
//4.获取新的socket连接
//5.接受数据
//6.发送数据
//7.关闭socket描述符
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

int main(int argc,char *argv[]) //将需要绑定的IP地址和port在命令行输出来
{
        if(argc!=3)
        {   
                printf("Usage:ip and port\n");
        }   
        //1.创建套接字
        int sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd<0)
        {   
                perror("sockfd error");
                return -1; 
        }   
        //2.绑定地址信息
        // int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in ser_addr;
        ser_addr.sin_family=AF_INET;
        ser_addr.sin_addr.s_addr=inet_addr(argv[1]);
        ser_addr.sin_port=(htons)(atoi(argv[2])); 
        int len=sizeof(struct sockaddr_in);
        int ret=bind(sockfd,(struct sockaddr*)&ser_addr,len);
        if(ret<0)
 {
                perror("binf error");
                close(sockfd);
                return -1;
        }

        //3.监听
        // int listen(int sockfd, int backlog);
        if(listen(sockfd,5)<0)//开始监听,接受客户端的连接请求,最大同时并发连接数为5
        {
                perror("listen error");
                close(sockfd);
                return -1;
        }
        //连接建立成功后,服务端会新建立一个socket
        while(1)
        {  //用while循环当一个连接断开后,可以重新获取新的socket
                //4.获取新建立的socket
                struct sockaddr_in cli_addr;
                len=sizeof(struct sockaddr_in);
                // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
                int newsockfd=accept(sockfd,(struct sockaddr*)&cli_addr,&len);//获取成功,返回新的socket描述符
                if(newsockfd<0)
                {
                        perror("newsockfd error");
                        return -1;
                }
                //连接建立成功:
                printf("new con:%s %d\n",(inet_ntoa)(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
                while(1)  //用循环是保证服务端可以和一个客户端可以多次聊天
                {
                        //5.发送数据 
                        //tcp协议:获取新的socket描述符后,新的socket里包含了服务端和客户端的地址信息,所以发送和接受数据没有先后之分
                        // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
                        char buff[1024]={0};
                        printf("please send data:");
 scanf("%s",buff);
                        ret=send(newsockfd,buff,strlen(buff),0); //阻塞发送数据 
                        if(ret<0)
                        {
                                perror("send error");
                                close(newsockfd);
                                return -1;
                        }
                        //6.接受数据
                        //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
                        memset(buff,0x00,1024);
                        len=recv(newsockfd,buff,1023,0);//0默认阻塞接受数据
                        if(len<0)//小于0接受失败
                        {
                                perror("recv error");
                                close(newsockfd);
                                continue;
                        }
                        else if(len==0)//等于0对端将连接断开
                        {
                                perror("peer has performed an orderly shutdown");
                                close(newsockfd);
                                continue;
                        }
                        printf("[%s:%d]->%s\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),buff);
                }
                close(newsockfd);
        }
        close(sockfd);
        return -1;
}

tcp客户端编程步骤
1.创建套接字
2.绑定地址信息(没有必要调用bind()绑定信息,否则一台机器上启动多个客户端,就会出现端口号被占用而导致不能正常连接)
3.向服务端发起连接请求
4.接受数据
5.发送数据
6.关闭
创建套接字、发送数据、接受数据即挂壁和服务端一样。
3.向服务端发送连接请求

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
addr:要连接的服务端地址
addrlen :地址信息长度
返回值: 成功; 0  失败 -1

客户端代码:

//tcp 客户端代码
//1.创建套接字
//2.绑定地址信息
//3.向服务端发送连接请求
//4.发送数据
//5.接受数据
//6.关闭socket描述符

#include<stdio.h>
#include<error.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<sys/socket.h>
#include<stdlib.h>

int main(int argc,char* argv[])
{
        if(argc!=3)
        {   
                printf("Usage ip and port\n");
        }   
        //1.创建套接字
        int sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd<0)
        {   
                perror("socket error");
                return -1; 
        }    
        //2.绑定地址信息(不推荐手动写绑定信息代码)
        //3.向服务端发送连接请求
        //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in ser_addr;
        ser_addr.sin_family=AF_INET;
        ser_addr.sin_port=(htons)(atoi(argv[2]));  //htons :主机字节序转换成网络字节序
        ser_addr.sin_addr.s_addr=(inet_addr)(argv[1]);//因为argv[]是char*,用atoi使字符串转成整型
        int len=sizeof(struct sockaddr_in);
int ret=connect(sockfd,(struct sockaddr*)&ser_addr,len);
        if(ret<0)
        {
                perror("connect error");
                close(sockfd);
                return -1;
        }
        //连接成功,socket描述符里有服务端和客户端IP地址和port
        while(1)
        {
                //4.接受数据
                //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
                char buff[1024]={0};
                ret=recv(sockfd,buff,1023,0);//默认阻塞接受数据
                if(len<0)//小于0接受失败
                {
                        perror("recv error");
                        close(sockfd);
                        continue;
                }
                else if(len==0)//等于0对端将连接断开
                {
                        perror("peer has performed an orderly shutdown");
                        close(sockfd);
                        continue;
                }
                // net_ntoa :网络字节序转换成点分十进制IP
                //ntohs  :主机字节序转换成网络字节序
                printf("[%s:%d]say:%s\n",(inet_ntoa)(ser_addr.sin_addr),(ntohs)(ser_addr.sin_port),buff);
                //4.发送数据
                // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
                memset(buff,0x00,1024);
  printf("please send\n");
                scanf("%s",buff);
                ret=send(sockfd,buff,strlen(buff),0); //默认阻塞接受数据
                if(ret<0)
                {
                        perror("send error");
                        close(sockfd);
                        return -1;
                }
        }
        close(sockfd);
        return 0;
}

客户端:
在这里插入图片描述
服务端:
在这里插入图片描述
当客户端ctrl+c断开连接后,服务端会提示对端已关闭,这时会有新的客户端建立连接。
listen参数和tcp最多建立连接数
int listen(int sockfd, int backlog);
backlog:
协议栈使用一个队列:这个队列的大小由listen系统调用的backlog参数决定。当一个syn包到达后,服务端协议栈回复syn+ack,然后将这个socket加入这个队列。当客户端第三次握手的ack包到达后,再将这个socket的状态改为ESTABLISHED状态。这也就意味着这个队列可以可以容纳两种不同状态的socket:SYN RECEIVED和 ESTABLISHED,而只有后者可以被accept调用返回。当队列中的连接数(socket)达到backlog个后,系统收到syn将不再回复syn+ack。这种情况下协议栈通常仅仅是将syn包丢掉,而不是回复rst报文,从而让客户端可以重试。
tcp最大连接数:
用ulimit -n 结果是1024,这表示当前用户的每个进程最多允许同时打开1024个文件,这1024个文件需要除去每个进程必然打开的标准输入、标准输出、标准错误、服务器监听socket,进程间通讯的unix域socket等文件,那么剩下可用于客户端socket连接的文件数就只有大概1024-10=1014个,即在缺省条件下,基于linux的通讯程序最多允许同时1014个TCP并发连接。

猜你喜欢

转载自blog.csdn.net/sophia__yu/article/details/82828273