基于tcp的简单socket编程

有段时间没有写网络这块的,最近学习下。

这篇帖子上介绍的linux下的基于tcp的socket编程不错,https://www.cnblogs.com/MyLove-Summer/p/5215287.html。
这里贴下一个主要的图,一个基于tcp的网络编程的图。
基于tcp的网络编程
这里来一个简单的例子,一个客户端ClientSock,一个服务端ServerSock,服务器这边监听的端口号是20000,让客户端连上服务器后发送数据,服务器收到后打印出来。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <cstdio>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <cstdlib>
#include <ctime>
#include <cstring>
#include <arpa/inet.h>

#define SERVER_PORT 20000
#define LENGTH_OF_LISTEN_QUEUE 10
#define BUFFER_SIZE 255

int main(int argc, char const *argv[])
{
int servfd,clifd;
struct sockaddr_in servaddr,cliaddr;
if((servfd = socket(AF_INET,SOCK_STREAM, 0)) < 0)
{
printf("创建服务端socket失败, servfd[%d]", servfd);
exit(1);
}

bzero(&servaddr , sizeof(servaddr));

servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
servaddr.sin_addr.s_addr =htons(INADDR_ANY);//INADDR_ANY就是指定地址为0.0.0.0

if(bind(servfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) < 0)
{
printf("绑定到端口%d失败 \n", SERVER_PORT);
exit(1);
}
if(listen(servfd, LENGTH_OF_LISTEN_QUEUE)<0)
{
printf("回调listen失败 \n");
exit(1);
}
while(1)
{
//服务端一直不退出,除非进程呗杀死
char buf[BUFFER_SIZE];
long timestamp;
socklen_t length = sizeof(cliaddr);
//accept是三次握手的服务端
clifd = accept(servfd, ( struct sockaddr *) &cliaddr, &length);

if(clifd < 0)
{
printf("回调accept的时候发生错误");
break; // 跳出循环
}
printf("来自客户端的ip:%s,端口号:%d,clifd[%d] \n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), clifd);
timestamp = time(NULL);
int lengthrec = 0;
lengthrec = recv(clifd, buf, BUFFER_SIZE, 0);//接受的是clifd
if(lengthrec < 0)
{
printf("接受客户端的消息失败, lengthrec[%d]\n",lengthrec);
exit(1);
}
printf("来自客户端的数据为[%s] \n",buf);

//server收到消息后再发一次消息
bzero(buf,sizeof(buf));
strcpy(buf,"server收到了client的消息,这是给你的ack回复");
send(clifd,buf,BUFFER_SIZE,0); //还是刚才的clifd
close(clifd);//关闭客户端的文件描述符
}
close(servfd);
getchar();
return 0;
}

本地测试的时候,为了不让终端一闪而过,最后加了个getchar()函数,让其停住。

2 客户端的代码

如下:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <cstdio>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <cstdlib>
#include <ctime>
#include <cstring>
#include <arpa/inet.h>

#define SERVER_PORT 20000
#define CLIENT_PORT ((20001+rand())%65536)
#define BUFFER_SIZE 255

void usage(char *name)
{
printf("usage:%s ",name);
}

int main(int argc, char *argv[])
{
int clifd,length =0;
struct sockaddr_in servaddr,cliaddr;
socklen_t socklen = sizeof(servaddr);
char buf[BUFFER_SIZE];
if(argc < 2)
{
usage(argv[0]);
exit(1);
}
if((clifd =socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("创建客户端的fd失败\n");
exit(1);
}
srand(time(NULL));

bzero(&cliaddr, sizeof(cliaddr));
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(CLIENT_PORT);
cliaddr.sin_addr.s_addr = htons(INADDR_ANY);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_aton(argv[1], &servaddr.sin_addr);
servaddr.sin_port = htons(SERVER_PORT);

if(bind(clifd , (struct sockaddr*) &cliaddr, sizeof(cliaddr)) < 0)
{
printf("绑定到端口%d失败\n", CLIENT_PORT);
exit(1);
}
if( connect(clifd, (struct sockaddr*)&servaddr, socklen) < 0)
{
printf("不能够连接到%s!\n", argv[1]);
exit(1);
}

strcpy(buf,"是服务器么,我是客户端");
send(clifd, buf, BUFFER_SIZE, 0); //clifd绑定的connect,发的话就是这个socket
printf("客户端发送完了,clifd[%d]\n", clifd);

bzero(buf,sizeof(buf));
recv(clifd,buf,BUFFER_SIZE,0); //接受消息也是同一个clifd
printf("client发完后又收到了server的消息,消息[%s]",buf);
close(clifd);
getchar();
return 0;
}

linux下还没有安啥ide,就用两个控制台来看
console1
可让服务器不关,client端多次运行
console2
这里服务器收到了是每次客户端不同的端口号。这样简单实现了一次通信,至于复杂的多次通信包括select poll epoll等可以继续包装。

3 关于socket中的send recv函数

在建立socket后收发数据的时候,用到了send() recv()函数,它们的第一个参数都是fd,这里开始看了一些网上各种抄的版本后,发现收不到数据,自己然后看了下源码和man手册

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
/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
Returns the number read or -1 for errors.

This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
ADDR_LEN bytes long). Returns the number sent, or -1 for errors.

This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
int __flags, __CONST_SOCKADDR_ARG __addr,
socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
the sender, and store the actual size of the address in *ADDR_LEN.
Returns the number of bytes read or -1 for errors.

This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
int __flags, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);

3.1 send函数

1
2
3
4
send(sockfd, buf, len, flags);
is equivalent to
sendto(sockfd, buf, len, flags, NULL, 0);
The argument sockfd is the file descriptor of the sending socket

send函数和sendto函数一样,sockfd是发送端的socket描述符。我们这里的例子只是client发数据server,所以是clifd.
源码中send是发送n个byte的buf到socket,这个socket就是发送端发送数据前建立的socket。

3.2 recv函数

网上的抄的都说第一个参数是指定接受端的套接字描述符。
源码解析从socket FD中读n个byte的buf,注意,这里的FD是三次握手建立连接的socket,也就是accept函数返回的socket,我们这里定义的是clifd。
针对发送端接收端,其实简单区分不太正确,发送也可接受,主要是看当前的数据socket是哪一条。|

理解清楚send和recv的socket是哪一个就知道fd了

ps>本地测试的时候有时候会发现端口号被占用,例如服务端的端口号20000被占用,
查看端口号是否被占用
netstat -anp|grep 20000
lsof - i:20000
杀进程
kill -9 pid号
ps:关于博客插入图片的参考https://blog.csdn.net/sugar_rainbow/article/details/57415705