TCP套接字编程 学习笔记 1
0. 套接字函数
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的三次握手,而且仅在连接建立成功或出错时才返回,其中出错可能有以下几种情况:
- 若TCP客户没有收到SYN分节的响应,则返回ETIMEOUT错误.会重复发送3次,如果3次都没有响应则返回该错误
- SYN响应为RST(表示复位),表示服务器在该端口上没有进程在等待连接,返回ECONNREFUSED错误.RST产生的3个条件:1.目的地为某端口的SYN到达,然而该商品上没有正在监听的服务器.
2.TCP想取消一个已有的连接
3.TCP接收到一个根本不存在的连接上的分节(TCPv1第246-250页有更详细的信息) - 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------失败
内核为任何一个给定的监听套接字维护两个队列
- 未完成连接队列:某个客户发出SY并到达服务器,而服务器正在等待完成相应的TCP三路握手过程.
- 已完成连接队列:每个已完成TCP三路握手过程的客户对应其中的一项.
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地址.在这样的调用中,套接字描述符参数必须是已连接套接字描述符,而不是监听套接字描述符