当前位置:   article > 正文

【UNIX网络编程】| 【02】基本TCP套接字编程(socket、connnect、bind、linsten、accept、并发服务器)_tcp bind connect

tcp bind connect

1、socket函数


#include <sys/types.h>
#include <sys/socket.h>
int socket(int family, int type, int protocol)
/**
 * func:创建通信端点(服务端)并返回一个描述符;
 * param family:指定通信域;选择用于通信的协议族;        
       AF_INET             IPv4 
       AF_INET6            IPv6
       AF_LOCAL   		   Unix域协议
       AF_ROUTE			   路由套接字
       AF_KEY			   密钥套接字
 * param type:指定通信语义;
	   SOCK_STREAM    [TPC]提供有序的、可靠的、双向的、基于连接的字节流。 可能支持带外数据传输机制;
       SOCK_DGRAM     [UDP]支持数据报(固定最大长度的无连接、不可靠消息);
     SOCK_SEQPACKET   [有序分组]为最大固定长度的atagram提供一个顺序的、可靠的、基于双向连接的数据传输路径; 消费
     				  者需要在每次输入系统调用时读取整个数据包;
       SOCK_RAW       [原始套接字]提供原始网络协议访问;
       SOCK_RDM        提供不保证顺序的可靠数据报层; 
     SOCK_NONBLOCK     在新打开的文件描述中设置O_NONBLOCK文件状态标志。 使用这个标志可以节省对fcntl(2)
     					的额外调用来实现相同的结果;
 * param protocol:当只有一个协议支持指定的套接字类型时,为0;其他需指定;
		IPPROTO_CP	   TCP传输协议
		IPPROTO_UDP	   UDP传输协议
		IPPROTO_SCTP   SCTP传输协议
 * return:成功返回新套接字的文件描述符,出错返回-1;
 * */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在这里插入图片描述

在这里插入图片描述

2、connect函数

int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen)
/**
 * func:将套接字连接到由addr指定的地址;
 * param sockfd:返回的文件描述符;
 * param addr:指向sockaddr结构的指针;若sockfd的类型是SOCK_DGRAM,则addr是默认情况下发送数据报的地
 				址,也是接收数据报的唯一地址; 若为SOCK_STREAM或SOCK_SEQPACKET,此调用将尝试与绑定到
 				addr指定地址的套接字建立连接;
 * param addrlen:传入结构体的大小;
 * return:成功返回0,失败返回-1;
 * */
- 套接字必须含有服务器的IP地址和端口号;客户在调用该函数不必调用bind函数,若需要,则内核会确定源IP地址,并选择
	一个临时端口为源端口;
- 该函数调用即TCP的三路握手过程,错误返回的几种情况:
	- 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误,超时无响应;
	- 若客户的SYN的响应是RST(复位),则该服务器主机在我们指定的端口上没有进程在等待与之连接;返回ECONNREFUSED错误;
	- 若客户发出的SYN在中间的某个路由器上引发目的不可达ICMP错误(软错误);
【产生RST的三条件】:
	- 目的地为某端口的SYN到达,而该端口上没有正在监听的服务器;
	- TCP想取消一个已有连接;
	- TCP接收到一个根本不存在的连接上的分节;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
2.1 案例
指定本机地址获取时间;【OK】
  • 1

在这里插入图片描述

指定本地子网上其主机ID并不存在的IP地址;当客户机发出ARP请求时,将不会收到;【TIMEOUT】
  • 1

在这里插入图片描述

指定一个没有运行时间获取服务器程序的主机【RST】
  • 1

在这里插入图片描述

指定英特网中不可达的IP地址【ICMP错误】
  • 1

在这里插入图片描述

注意

- connect函数导致当前套接字从CLOSED状态,转移到SYS_SENT状态,若成功则再转移到ESTABLISHED状态,若connect失败
则该套接字不再可用,必须关闭,不能对这样的套接字再次调用connect函数;
- 当调用connect调用失败后,必须close当前的套接字描述符并重新调用socket;
  • 1
  • 2
  • 3

3、bind函数

int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen)     
/**
 * func:当使用socket创建套接字时,它存在于一个名称空间(地址族)中,但没有分配给它地址。 Bind()将addr
 * 		指定的地址分配给套接字;
 * param sockfd:返回的文件描述符;
 * param addr:传入sockaddr_in结构体且需要进行强制转换为(sockaddr);
			serv_addr.sin_family = AF_INET;			// 通信域
     		serv_addr.sin_port = htons(SERV_PORT);	// 端口
     		serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);	// IP
 * param addrlen:传入addr的长度;
 * return:成功返回0,失败返回-1;
 * */
【bind可指定一个端口号或IP地址或两者都可指定】
- 【TCP客户没有捆绑端口】:
	当调用connect或listen时,内核会为响应的套接字选择一个临时端口;
	- 如RPC服务器,内核为其选择临时端口,而端口通过RPC端口映射器进行注册,客户再connect之前,必须与端口映射器联系以获取它们的临时端口;
- 【捆绑IP地址】:
	对于TCP客户,为该套接字上发送的IP数据报指派了源IP地址;
	对TCP服务器,就限定该套接字只接收那些目的地为该IP地址的客户连接;
	TCP客户通常不把IP地址捆绑到它的套接字上,当连接套接字时,内核将根据所用外出网络接口来选择源IP地址,
		而外出接口这取决于到达服务器所需的路径;
	若TCP服务器没有捆绑IP地址,内核及那个客户发送的SYN目的IP地址作为服务器的源IP地址;
	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在这里插入图片描述

3.1 通配地址
IPv4一般使用来INADDR_ANY(0)来指定
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/**
由于INADDR_常值一般都为主机字节序定义,故对此类常值需要使用htonl
*/

IPV6一般为128struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any;

如果让内核来选择一个临时端口,bind无法获取,必须使用getsockname来获取;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
3.2 运行捆绑通配地址的当个服务器
当一个连接到达时,服务器调用getsockname获取客户的目的IP地址,服务器在根据这个客户连接所发往的IP地址来
处理客户的请求;
【好处】:把一个给定的目的IP地址解复用到一个给定的服务器进程时是由内核完成的;
必须细分一个分组到达接口和该分组的目的IP地址;若使用弱端系统模型,则意味着一个分组只要其目的IP地址能够标识
目的主机的某个网络接口即可,不一定是它的到达接口;
捆绑非通配IP地址只是限定根据目的IP地址来确定递送到套接字的数据报,而对于到达接口则未做任何限制,除非系统采
用强端系统模型;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

4、listen函数

#include <sys/socket.h>

int listen(int sockfd, int backlog)
/**
 * func:将套接字标记为被动套接字,即用于accept接收传入的连接请求的套接字;
 * param sockfd:返回的文件描述符;
 * param backlog:最大了连接数;
	- 内核为一个给定的监听套接字维护两个队列:
		【未完成连接队列SYN_RCVD】:每个这样的SYN分节都对应某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手;
		【已完成连接队列ESTABLISHED】:每个已完成TCP三路握手过程的客户对应其中一项;
		当未完成连接队列时中创建一项时,来自监听套接字的参数本复制到将建立连接中;连接的创建机制是完成自动的,
 * return:成功返回0,失败返回-1;
【注意】:不要将backlog设置为0,若不想客户链接,关闭套接字即可;
 * */
【该函数仅由TCP服务器调用,做以下两件事】:
- 当socket创建一个套接字时,它时一个将调用connect发起连接的客户套接字;该函数把一个为连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求;
	【调用导致套接字从CLOSED状态转换到LISTEN状态】
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在这里插入图片描述
在这里插入图片描述

若三路握手正常,该项将从未完成连接队列中的队头移到已完成连接队列的队尾,当accept调用时,已完成连接队列中的队头项将返回给进程,若该队列未空,则进程被投入睡眠;
  • 1
4.1 listen包裹函数,设置backlog
void Listen(int fd, int backlog){
	char *ptr;
	if((ptr) = getenv("LISTENQ") != NULL) 
		bakclog = atoi(ptr);
	if(listen(fd, backlog) < 0)
		exit(-1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述

5、accept函数

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
/**
 * func:侦听套接字sockfd的挂起连接队列中提取第一个连接请求,创建一个新的连接套接字,并返回一个指向该套
 * 		接字的新文件描述符;
 * param sockfd:返回的文件描述符;
 * param addr:指向sockaddr结构的指针,为传出参数;
 * param addrlen:为值-结果参数(可查看上一篇文章);
 * return:成功返回文件描述符,失败返回-1;
 * */
若无需客户的协议地址,可将addr和addrlen设置为NULL
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
5.1 服务端程序:通过accept获取客户端ip和端口
/**
运行时
*/
#include "../Jxiepc/unp.h"
#include <iostream>

using namespace std;

int main() {
    int lfd, cfd;
    socklen_t len;
    struct sockaddr_in s_addr, c_addr;
    char buf[128];
    time_t ticks;

    /* 创建套接字 */
    lfd = socket(AF_INET, SOCK_STREAM, 0);

    /* 绑定端口、IP */
    bzero(&s_addr, sizeof(s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    s_addr.sin_port = htons(13);
    bind(lfd, (struct sockaddr*)&s_addr, sizeof(s_addr));

    /* 设置监听个数 */
    listen(lfd, 128);

    while (true) {
        /* 接收客户端 */
        len = sizeof(c_addr);
        cfd = accept(lfd, (struct sockaddr*)&c_addr, &len);
        /* 获取客户端ip、端口 */
        cout << "connect from " <<
            inet_ntop(AF_INET, &c_addr.sin_addr, buf, sizeof(buf))
            << ",  port " << ntohs(c_addr.sin_port) << endl;
        /* 回写时间 */
        ticks = time(NULL);
        snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
        write(cfd, buf, strlen(buf));
        close(cfd);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

在这里插入图片描述

在这里插入图片描述

6、fork和exec函数

#include <unistd.h>

pid_t fork(void);
/*
* @func: 该函数调用一次返回两次,父进程中返回子进程的ID号,子进程中返回0;通过判断返回值来确定是否为子进程;
		- 在子进程中,阔以通过getppid来获取父进程ID;父进程可以有多个子进程,但无法获取各个子进程的ID(需要对每个进行跟踪);
		- 父进程中调用fork之前打开的所有描述符在fork返回后由子进程分享;
		- 服务器可利用父进程中调用accept后调用fork,使用子进程来处理客户;
* return: 失败返回-1,成功返回:父进程返回子进程的ID以及子进程返回0;
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
6.1 fork的典型用法
- 一个进程创建一个自身的副本,都可在另一个副本执行其他任务的同时处理各自的某个操作;
- 一个进程想要执行另一个程序,在子进程中调用exec把自身替换成新的程序;
  • 1
  • 2
6.2 exec函数
#include <unistd.h>

int execl(const char *pathname, const char *arg0, ...);
int execv(const char *pathname, char *const *argv[]);
int execle(const char *pathname, const char *arg0, ...);
int execve(const char *pathanem, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ...);
int execvp(const char *filename, char *const argv[]);

/**
	上述函数中,只有函数错误时,才返回调用者;
	只有execve是系统调用,其余为库函数;
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这里插入图片描述
以上函数的区别

- 上图中上行的函数把新程序的每个参数字符串指定成exec的一个独立参数,并以空指针结束的可变参数;
	下行以argv为参数数组,其中含有指向新程序各个参数字符串的所有指针;
- 左列两个函数以filename为参数,指定文件名;右两列四个函数以pathname为参数;
- 左两列函数不显示指定一个环境指针;它们使用外部变量environ的当前值构造一个传递给新程序的环境列表;
  • 1
  • 2
  • 3
  • 4

注意

在exec前打开的描述符在exec后依旧为打开状态,但可以通过fcntl设置FD_CLOEXEC禁止;
  • 1

7、并发服务器

最简单的方法是使用fork创建子进程来服务每个客户;
- 当调用accept返回时,就使用fork由子进程来服务客户,父进程用来等待连接;
  • 1
  • 2
7.1 结合fork的大致用法
pid_t pid;
int lfd, cfd;

bind(lfd, ...);
listen(lfd, 128);
while(1) {
	cfd = accept(lfd, ...);
	if(pid == fork()) == 0) {
		close(lfd);
		doit(cfd);
		close(cfd);
		exit(0);
	}
	/**
	当连接建立,accpet返回,调用fork,由子进程服务客户(cfd),父进程等待新连接,由于客户由子进程提供服务,故在父
		进程中需要关闭已经连接的套接字;
	*/
	close(cfd);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
7.2 为什么父进程对cfd调用close没有终止它与客户的连接呢?
每个文件或套接字都由一个引用计数,维护在文件标项中,是当前打开着的引用描述符的个数;
socket返回后与lfd关联的文件引用数为1,accept与cfd关联的文件引用数也为1;在fork中,该两
个描述符在父进程与子进程间共享,故两个描述符的引用数变为2,而父进程中关闭cfd,只是将
引用计数从2减到1;
- 而真正的清理和资源释放要等到为0时才发生;
- 若父进程对accept返回的已连接套接字不调用close则将会耗尽可以描述符;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

8、close函数

#include <unistd.h>

int close(int sockfd);      
/**
@func: 关闭套接字,并终止TCP连接;
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
8.1 描述符引用计数
上述中,close减少描述符的计数,有时并不会引发TCP终止序列;
若想要TCP发生FIN,则改用shutdown函数;
  • 1
  • 2

9、getsockname和getpeername函数

#include <says/socket.h>

int getsockname(int sockfd, strict sockaddr *localaddr, socklen_t *addrlen);
/**
@func: 
	- 在没有bind的TCP客户上,connect成功后,该函数用于返回由内核赋予连接的本地
		IP地址和本地端口号;
	- 在以端口号0调用bind(由内核去选择本地端口号)后,该函数用于返回由内核赋予的本地端口号;
	- 可用于获取某个套接字地址族;
	- 在一个以通配地址调用bind的tcp服务器上,与某客户连接建立后,getsockname可用于返回由内核赋予该连接的本地IP地址;
	- 当一个服务器是由调用过accept的某个进程通过调用exec,该函数可获取客户身份;
*/
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
/**
@func: 
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
描述符的传递:
	- 调用exec的进程可以把这个描述符格式化成一个字符串,作为命令行参数传递;
	- 约定exec之前,总是把某个特定描述符置为所接受的已连接套接字的描述符;
  • 1
  • 2
  • 3
7.2 获取套接字的地址族
int sockfd_to_family(int sockfd) {
	
	struct sockaddr_storage ss;
	socklen_t len;
	len = sizeof(ss);
	if(getsockname(sockfd, (struct sockaddr*)&ss, &len) < 0)
		return -1;
	return ss.sin_family;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/625307
推荐阅读
相关标签
  

闽ICP备14008679号