TCP套接字编程 学习笔记 1

时间:2021-08-16 00:44:50

0. 套接字函数

TCP套接字编程 学习笔记 1

1.socket函数

为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型(IPv4的TCP,IPv6的UDP,Unix域字节流协议等)

#include<sys/socket.h>
int socket ( int family , int type , int protocol);
返回 : 非负的描述符
------ 成功,-1 ----- 失败-

family : 指明协议族,    (AF_INET,IPv4协议)  ,   (AF_INET,IPv6协议),......

type:指明套接口类型,  (SOCK_STREAM,字节流套接口),(SOCK_DGRAM,数据报套接口),......

protocol:指明协议类型常值,(IPPROTO_TCP,TCP传输协议),(IPPROTO_UDP,UDP传输协议),(IPPROTO_SCTP,SCTP传输协议),(0,family和type组合的系统缺少值).

  成功则返回一个套接口描述字

 2.connect函数

TCP客户用connect函数来建立与TCP服务器的连接

#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen)
返回:
0------成功 -1 ------失败

sockfd : socket函数返回的套接字描述符

servaddr:套接口地址结构(必须含有服务器的地址和端口号)的指针

addrlen:该结构的大小

如果客户端没有绑定端口号,则内核会确定源IP地址,并选择一个临时端口作为源端口

如果是TCP套接口,调用connect函数将激发TCP的三次握手,而且仅在连接建立成功或出错时才返回,其中出错可能有以下几种情况:

  1. 若TCP客户没有收到SYN分节的响应,则返回ETIMEOUT错误.会重复发送3次,如果3次都没有响应则返回该错误
  2. SYN响应为RST(表示复位),表示服务器在该端口上没有进程在等待连接,返回ECONNREFUSED错误.RST产生的3个条件:1.目的地为某端口的SYN到达,然而该商品上没有正在监听的服务器.
                                   2.TCP想取消一个已有的连接
                                   3.TCP接收到一个根本不存在的连接上的分节(TCPv1第246-250页有更详细的信息)
  3. SYN在中间的某个路由器上引发一个目的地不可达的ICMP错误,主机内核保存错误并继续发送SYN,若在某个规定的时间后仍未收到响应,则把保存的消息作为
     EHOSTUNREACH或ENETUNREACH错误返回给进程.

3.bind函数

bind函数把一个本地协议地址赋给一个套接口.

本地协议地址(32位IPv4或128位的IPv6,16位UDP或TCP端口号)

#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen);
返回:
0------成功,-1------失败

myaddr:套接口地址结构的指针

addrlen:该结构的大小

调用bind可以指定IP地址和端口,可以两者都指定,也可以都不指定

 

IP地址 端口 结果
通配地址 0     内核选择IP地址和端口
通配地址 非0 内核选择IP地址,进程选择端口
本地IP地址 0 进程选择IP地址,内核选择端口 
本地IP地址 非0 进程指定IP地址和端口

通过getsockname()来返回实际的协议地址

 

4.listen函数

listen函数仅由TCP服务器调用,它做两件事情:

1.当socket函数创建一个套接口时,它被假设为一个主动的套接口,也就是说,它是一个即将调用connect发起连接的客户端套接口.listen函数把一个未连接的套接口转换成为一个被动的套接口,指示内核应当接收指向该套接口的连接请求.调用listen寻到套接口由CLOSE状态转换为LISTEN状态

2.第二个参数指定内核应该为相应套接口排队的最大连接个数

#include<sys/socket.h>
int listen(int sockfd,int backlog);
返回:
0------成功,-1------失败

内核为任何一个给定的监听套接字维护两个队列

  1. 未完成连接队列:某个客户发出SY并到达服务器,而服务器正在等待完成相应的TCP三路握手过程.
  2. 已完成连接队列:每个已完成TCP三路握手过程的客户对应其中的一项.

TCP套接字编程 学习笔记 1

 

5.accept函数

accept函数由TCP服务器调用,用于从已完成连接队列队头取出下一个已完成连接.如果已完成连接队伍为空,那么进程被投入睡眠(假设为阻塞方式)

#include<sys/socket.h>
int accept(int sockfd,struct sockaddr* cliaddr,socklen_t* addrlen);
返回:非负描述符
------成功,-1------失败


cliaddr:接收对端客户的协议地址.

addrlen:调用前整数值置为cliaddr地址结构的大小,调用后返回实际接口地址结构内确切的字节数

若返回成功,则内核为每个已连接的客户创建一个已连接的套接口.

 

简单的服务器时间回显程序

#include<time.h>
#include
<iostream>
#include
<sys/socket.h>
#include
<netinet/in.h>
#include
<stdlib.h>
#include
<stdio.h>
#include
<errno.h>
#include
<string.h>
#include
<cassert>
#include
<unistd.h>
#include
<arpa/inet.h>
using std::cout;
class TCP
{
public:
bool Socket()
{
listenfd
= socket(AF_INET,SOCK_STREAM,0);
return listenfd >= 0;
}
bool Bind(int port)
{
memset(
&servaddr,0,sizeof(servaddr)); //清空结构体
servaddr.sin_family = AF_INET; //指定协议
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //通配地址
servaddr.sin_port = htons(port); //指定端口号
int bind_ret = -1;
if(listenfd >= 0)
bind_ret
= bind(listenfd,(sockaddr *)&servaddr,sizeof(servaddr));//绑定指定端口
else
{
cout
<< "listenfd < 0 !!!\n";
return false;
}

return bind_ret == 0;
}

bool Listen(int len){
return listen(listenfd,len) == 0;
}

int Accept()
{
socklen_t len
= sizeof(cliaddr);
int connfd = accept(listenfd,(sockaddr *)&cliaddr,&len); //从已完成连接队列取出首个,若没有则阻塞
cout << "connect from " << inet_ntop(AF_INET,&cliaddr.sin_addr,buf,sizeof(buf))
<< ", port " << ntohs(cliaddr.sin_port) << "\n";
return connfd;
}

bool Close(int connfd)
{
close(connfd);
return true;
}

private:
int listenfd;
struct sockaddr_in servaddr,cliaddr;
char buf[1024];
};

int main()
{
TCP tcp;
bool r = tcp.Socket();
assert(r
== true);
r
= tcp.Bind(1027);
assert(r
== true);
r
= tcp.Listen(30);
while(true){
int connfd = tcp.Accept();
time_t ticks
= time(0);
char buf[128];
snprintf(buf,
sizeof(buf),"%.24s\r\n",ctime(&ticks));
write(connfd,buf,strlen(buf));
tcp.Close(connfd);
}
return 1;
}

 

6.fork()函数

#include<unistd.h>
pid_t fork(
void);
返回:在子进程中返回0,父进程返回子进程的ID,若出错返回
-1

fork在子进程返回0的原因在于子进程可以通过getppid()取得父进程的进程ID.相反,父进程可以有许多子进程,而且无法获取各个子进程的进程ID,如果父进程想要跟踪子进程的进程ID,那么它必须记录每次调用fork返回值.

父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享.我们将看到网络服务器利用这个特性:父进程调用accept之后调用fork.所接受的已连接套接字随后就在父进程和子进程之间共享.通常情况下,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字.

 

并发服务器

仔细观察简单的服务器时间回显程序的代码,服务器在处理一个客户的连接时,其它客户连接就要等待.如果一个客户长期占用服务器,那么其它客户将得不到服务.

解决办法是使用并发,方法如下

int main()
{
TCP tcp;
bool r = tcp.Socket();
assert(r
== true);
r
= tcp.Bind(1027);
assert(r
== true);
r
= tcp.Listen(30);
while(true){
int connfd = tcp.Accept();
int pid = fork();
if(pid == 0)
{
tcp.Close(tcp.Getlistenfd()); //引用计数-1,并不会真正关闭
time_t ticks
= time(0);
char buf[128];
snprintf(buf,
sizeof(buf),"%.24s\r\n",ctime(&ticks));
write(connfd,buf,strlen(buf));
tcp.Close(connfd);
exit(
0);
}
tcp.Close(connfd);
}
return 1;
}

close()函数

#include<unistd.h>
int close(int sockfd);
返回:若成功则为0,若出错则为
-1

getsockname和getpeername函数

返回某个套接字相关的本地协议地址或外地协议地址

#include<sys/socket.h>
int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen);
int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen);
返回:若成功则为0,若出错则为
-1

需要这两个函数的理由如下:

1.在没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋给的本地IP地址和本地端口号

2.在以端口号0调用bind(告知内核去选择本地端口号)后,getsockname用于返回内核赋给的本地端口号

3.getsockname可用于获取某个套接字的地址族

4.在一个以通配IP地址调用bind的TCP服务器上,与某个客户的连接一旦建立,getsockname就可以用于返回由内核赋给该连接的本地IP地址.在这样的调用中,套接字描述符参数必须是已连接套接字描述符,而不是监听套接字描述符