基础知识

socket && socket pair

socket是 IP号 + 端口号。

因此,在一个网络中,每个socket都是一个唯一的进程。

而TCP协议中,建立连接的两个进程分别用2个socket表示,这两个socket组成一对"socket pair"。

可以看出 这个 "socket pair" 即是一个唯一的连接,而他们之间通信的本质,也正是进程间通信


大端小端:

大端 小端是内存中存放数据的两种方式,其中:

大端是 高位数据存放在低地址,低位数据存放在高地址

小端反之。

而网络数据传输采用的是大端,因此在发送数据和接收数据的时候都要进行判断。如果不统一,则需要进行转换。

至于在实际编写代码的时候,下面是现成的轮子:

uint32_t htonl(uint32_t hostlong);//将一个无符号短×××数从网络字节顺序转换为主机字节顺序
uint32_t htons(uint32_t hostshort);//将一个无符号长×××数从网络字节顺序转换为主机字节顺序
uint32_t ntohl(uint32_t netlong);//将主机的无符号短×××数转换成网络字节顺序
uint32_t ntohs(uint32_t netshort);//将主机的无符号长×××数转换成网络字节顺序





调用网络库的函数:

socket()  //打开网络端口
bind()    //将sockfd和地址绑定,并用于网络通信
listen()  //声明sockfd处于监听的状态
connect() //建立连接(也就是三次握手的步骤,当然是由客户端发出)(connect到的地址就是server中 bind()的地址)
accept()  //接受连接 (不属于三次握手,而是三次握手之后的通信)






—————————————————————

代码实现

工作流程:

Server端一直挂起,等待客户端的连接,一旦建立连接,开始通信


Server端


三种模式:

单执行流的Server端每次只能和一个Client端连接

多进程的Server能同时和多个Client连接,但开销较大

多线程的Server用多个线程的方式同时和多个Client端连接,我的Server端就采用的是这种模式



Server建立连接并进行通信主要分为4个步骤:

1、创建 socket

2、将socket 和 Server的IP、端口号进行绑定

3、设置为监听状态

4、每建立一个链接,就开启一个新的线程,在线程内部与对应的Client端进行通信

    

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

//server

static void usage(const char *proc )
{
	printf("Usage: %s [ip] [port]\n", proc);
}

void* thread_run(void *arg)
{
	printf("create a new thread\n");
	int fd = (int)arg;
	char buf[1024];
	while(1)
	{
		memset(buf, '\0', sizeof(buf));
		ssize_t _s = read(fd, buf, sizeof(buf) - 1 );
		if( _s > 0 )
		{
			buf[_s] = '\0';
			printf("client :# %s\n", buf);

			memset(buf, '\0', sizeof(buf));
			printf("please enter: ");
			fflush(stdout);
			_s = read(0, buf, sizeof(buf) - 1);
			if(_s > 0)
			{
				buf[_s - 1] = '\0';
				write(fd, buf, strlen(buf) );
			}
		}
		else if(_s == 0)
		{
			printf("client close...\n");
			break;
		}
		else
		{
			printf("read error...\n");
			break;
		}
	}
}

int main(int argc, char *argv[] )
{
	if(argc != 3 )
	{
		usage(argv[0]);
		exit(1);
	}


	//1.CreateSocket
	int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if(listen_sock < 0)
	{
		perror("socket");
		return 2;
	}

	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(atoi(argv[2]));  //argv[2]
	local.sin_addr.s_addr = inet_addr(argv[1]);

	//2.Bind
	if( bind(listen_sock, (struct sockaddr*)&local, sizeof(local) ) < 0)
	{
		perror("bind");
		return 3;
	}

	//3.Always Listen
	listen(listen_sock, 5);

	//4.Accept
	struct sockaddr_in peer;
	socklen_t len = sizeof(peer);
	while(1)
	{
		int fd = accept(listen_sock, (struct sockaddr*)&peer, &len);
		if(fd < 0)
		{
			perror("accept");
			return 4;
		}

		printf("get a new link... socket -> %s : %d\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));

		pthread_t id;
		pthread_create(&id, NULL, thread_run, (void*)fd);
		pthread_detach(id);

	}
	return 0;
}




Client端:


Client端建立连接并进行通信主要分为4个步骤:

1、创建Socket

2、连接

3、通信


#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

//client

static void usage(const char *proc )
{
	printf("Usage: %s [ip] [port]\n", proc);
}

int main(int argc, char *argv[])
{
	if(argc != 3 )
	{
		usage(argv[0]);
		exit(1);
	}
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0)
	{
		perror("socket");
		return 2;
	}
	struct sockaddr_in remote;
	remote.sin_family = AF_INET;
	remote.sin_port = htons(atoi(argv[2]));
	remote.sin_addr.s_addr = inet_addr(argv[1]);

	if( connect(sock, (struct sockaddr*)&remote, sizeof(remote) ) < 0)
	{
		perror("connect");
		return 3;
	}

	char buf[1024];
	while(1)
	{
		printf("Please Enter: ");
		fflush(stdout);
		ssize_t _s = read(0, buf, sizeof(buf) - 1);

		if(_s > 0)
		{
			buf[_s - 1] = '\0';
			write(sock, buf, strlen(buf));
			_s = read(sock, buf, sizeof(buf));
			if( _s > 0)
			{
				buf[_s] = '\0';
				printf("%s\n", buf);
			}
		}
	}
	return 0;
}



—————————————————————

总结&&一些值得注意的事情

一、汇总Server 和 Client的步骤:

Server端:

1、创建 socket

2、将socket 和 Server的IP、端口号进行绑定

3、设置为监听状态

4、等待,直到收到Client端的连接请求每建立一个链接,就开启一个新的线程,在线程内部与对应的Client端进行通信


Client端:

1、创建Socket

2、连接

3、通信



二、为什么不用void*

socket编程采用sockaddr*强制类型转换的方式对其家族的类型进行传参

当然相比,void*的通用性更强一些,理论上用void*设计更好,

但socket网络库要早于ANSI规范的提出,所以那时候并没有void*.......




三、Client端的socket没必要进行绑定


client端的端口号并不需要像server端那样固定以方便连接,因此不需要绑定,

而由于没有调用bind,client的端口号是由内核自动分配的


四、”Address already in use“


如下图:

Linux下搭建一个简单的TCP通信_Linux

建立连接,然后关闭server端,再打开,会提示这样,这是因为:

server程序终止了,但TCP协议层的连接并未完全断开,当前IP被占用了。

TCP协议规定,主动关闭连接的一方要处于 TIME_WAIT 状态,等待两个MSL的时间才回到CLOSE的状态。

MSL的具体时间由操作系统决定,通常 server主动关闭2分钟后 就可以再次启动了

当然,如果等不及的话,也可以用setsockopt,


使用setsockopt,使得socket可以被重用

具体的做法为,在socket调用和bind调用之间加上一段对socket的设置:

int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

这段代码的设置,表示允许创建端口号相同但IP地址不同的多个socket描述符




五、完整的实现


https://github.com/HonestFox/NetWorks/tree/TCP