tcp网络编程
tcp网络编程步骤:
由于tcp传输特点是可靠有连接,那么就有
1.客户端向服务端发送连接请求(SYN),
2.服务端接受请求并向客户端发送(SYN+ACK);
3.客户端向服务端回复ACK表明他知道服务端同意连接。
以上三个步骤就是三次握手。
服务端编程步骤:
1.创建套接字
2.为套接字绑定地址信息
3.监听:开始接受服务端的连接请求
4.获取连接建立成功的新socket
5.发送数据
6.接受数据
1.创建套接字
#include
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
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或#include中定义。该结构体解决了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 /* See NOTES */
#include
int listen(int sockfd, int backlog);
listen()声明sockfd处于监听状态,并且最多允许backlog个客户端处于连接等待状态,如果接受到更多的连接请求就忽略,一般是5,即代表最大同时并发连接数为5,这个数字并不是tcp最大建立连接数。(文章后面会讲述tcp最大建立连接数)
返回值:成功: 0 失败 -1
4.accept():获取新建立的socket
#include
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
#include
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
#include
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
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
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并发连接。