Java学习小记——多线程&Socket编程

线程池

线程池介绍

在创建一个线程时存在一定的开销,创建线程的开销相比于一般的算法来说较大。首先需要建立一个调用栈,接着操作系统还需要建立很多数据结构来维护线程的状态等等。为了解决每次都需要临时创建线程带来开销的问题,引入了线程池。

线程池会预先建立好线程,等待任务派发。一个线程池内可能会有几十上百个闲置的线程,当有任务来临时,需要选中一个线程执行任务。执行完毕后释放该线程资源,重新回到闲置状态,等待下一个任务。

在线程池中,通常有一个Blocking Queue用来派发任务。队列通常为空,当有新的任务进队列时,闲置线程开始抢占该任务。当没有闲置线程时,新任务到达Blocking Queue便会开始排队,等待有线程空出来。

线程池的参数

corePoolSize:线程池中初始线程的数量,可能处于等待状态
maimumPoolSize:线程池中最大允许线程数量
keepAliveTime:超出corePoolSize部分线程如果等待这些时间,就会被回收

Java线程池ExecutorTester

可以使用ThreadPoolExecutor创建线程池:
在这里插入图片描述
参数包含了corePoolSize,maimumPoolSize,keepAliveTime,时间单元TimeUnit以及一个可执行的workQueue任务队列。

另一种写法是用executors.newFixedThreadPool()创建线程池。这样的写法参数较少,只需要指定一个线程数即可。

ExecutorService executor = Executors.newFixedThreadPool(3);

如此创建的线程池,拥有的线程数量固定为3,如果任务数大于3,那么多余的任务则只有排队等待。

接下来我们一口气给这个线程池派发10个任务试试:

for (int i = 0; i < 10; i++) {
	executor.submit(new CodingTask(i));
}
System.out.println("10 tasks dispatched successfully.");

在这里插入图片描述
由于线程池参数为3,所以会按3个一组来抢占任务。
在这里插入图片描述
第一轮派发完毕后:
在这里插入图片描述
可以看到将3个任务派发给了线程们,workQueue中还剩7个任务。

Future<?>可以用来控制任务的执行:
在这里插入图片描述
在这里插入图片描述
cancel(boolean):可以临时中止执行任务
get():若成功执行则返回null,否则等待
iscancelled():获取任务是否中止
isDone():获取任务是否成功执行

ExecutorService executor = Executors.newFixedThreadPool(3);

    List<Future<?>> taskResults = new LinkedList<>();
    for (int i = 0; i < 10; i++) {
      taskResults.add(executor.submit(new CodingTask(i)));
    }
    System.out.println("10 tasks dispatched successfully.");

    for (Future<?> taskResult : taskResults) {
      taskResult.get();//get等待task结束,一旦结束会返回null
    }

服务器socket编程

普通socket编程

我们设想这样一个场景,一个服务器监听在6666端口上。若客户发aaa,则响应Hello aaa。首先我们需要建立一个服务端socket去监听端口号:

ServerSocket serverSocket = new ServerSocket(6666)

然后建立一个客户端socket,用getRemoteSocketAddress()方法获取远端socket端口号:

Socket clientSocket = serverSocket.accept();
System.out.println("Incoming connection from "
            + clientSocket.getRemoteSocketAddress());

最后用clientSoclet提供的getInputStream()方法和getOutputStream()方法实现开头我们所设想的功能:

try(ServerSocket serverSocket = new ServerSocket(6666)){
			System.out.println("listening on "+serverSocket.getLocalSocketAddress());
			Socket clientSocket = serverSocket.accept();
			System.out.println("Incoming connection from "+clientSocket.getRemoteSocketAddress());
			try(Scanner input = new Scanner(clientSocket.getInputStream())){
				String request = input.nextLine();
				System.out.println(String.format("Request from %s: %s",
						clientSocket.getRemoteSocketAddress(),
						request));
				String response = "Hello"+request+".\n";
				clientSocket.getOutputStream().write(response.getBytes());
			}
		}

执行以上代码并在命令行执行:

telnet localhost 6666

在这里插入图片描述
输入aaa后,服务端返回消息:
在这里插入图片描述
在这里插入图片描述
当然,正常的服务器不会在处理完一个请求后就马上切断与客户端的连接。所以,我们需要在把处理请求块放进一个循环中,并且加入终止条件即可。

try(ServerSocket serverSocket = new ServerSocket(6666)){
			System.out.println("listening on "+serverSocket.getLocalSocketAddress());
			Socket clientSocket = serverSocket.accept();
			System.out.println("Incoming connection from "+clientSocket.getRemoteSocketAddress());
			try(Scanner input = new Scanner(clientSocket.getInputStream())){
				while(true) {
					String request = input.nextLine();
					if(request.equals("quit")) {
						break;
					}
					System.out.println(String.format("Request from %s: %s",
							clientSocket.getRemoteSocketAddress(),
							request));
					String response = "Hello "+request+".\n";
					clientSocket.getOutputStream().write(response.getBytes());
				}
			}
		}

在这里插入图片描述
但是此时的服务器只能处理一个客户的请求。当另开一个控制台telnet 6666端口时,由于已经被占用,其他客户无法接受服务。

线程池并行处理客户请求

线程池在前面已经介绍过,它预先建立好了线程,等待任务来临。在线程池中有一个Blocking Queue,任务到来后会进入该队列,当到达队头并且有闲置进程时就会被选中执行。
在这里插入图片描述

在Java中,扔进Blocking Queue的是一个个Client Handler,因为每个client会占据一个线程,所以Client Handler即代表待执行的任务。线程调用Client Handle的run()方法来执行该任务。

	ExecutorService executor = Executors.newFixedThreadPool(3);
    RequestHandler requestHandler = new RequestHandler();

    try (ServerSocket serverSocket = new ServerSocket(7777)) {
      System.out.println("Listening on "
          + serverSocket.getLocalSocketAddress());

      while (true) {
        Socket clientSocket = serverSocket.accept();
        System.out.println("Incoming connection from "
            + clientSocket.getRemoteSocketAddress());
        executor.submit(
            new ClientHandler(clientSocket, requestHandler));
      }
    }

在这里插入图片描述

此时,服务器端就就可以支持多线程工作,因为线程池最大为3,所以最多可以同时开3个命令行访问服务器。当第4个命令行请求服务时,仍可连接成功,但需等待前3个服务中至少有一个服务退出,才可以接受服务。

Java NIO异步处理客户请求

每个线程处理一个客户请求看似十分合理,实则也存在缺点。因为线程池的容量有限,一个几十上百线程的线程池最多也只能服务几十上百个客户,难以实现大容量高吞吐的服务端。

在上一节的服务端线程池中,看似是所有线程都在忙着应付手头上的任务,而无暇顾及新到的任务,然而事实真的如此吗?比如当线程等待用户的输入时,用户的输入可以非常慢,这时线程便会被阻塞,并非在忙碌工作着。

因此,我们是否可以将所有client的input记住,再去检查谁输入完毕,就先去读这个输入并处理。也就是说,我们应该把一个个的request去交给线程处理,而不是把整个client都交给线程去处理。这样就避免了整个线程的等待时间,即将同步处理优化为了异步处理,这就是NIO异步服务器。
在这里插入图片描述

在NIO异步服务器中,等待的任务不再进入一个Blocking Queue,而是进入一个Channel列表。列表中有很多的Client,在每个循环开始时,通过一个Selector选择器去选择一个有数据的Channel,接着Request Handle去处理这个有数据的Channel,如此循环。

	ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.configureBlocking(false);
    serverChannel.bind(new InetSocketAddress(8888));
    System.out.println("Listening on "
        + serverChannel.getLocalAddress());

    Selector selector = Selector.open();
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    RequestHandler requestHandler = new RequestHandler();
    while (true) {
      int selected = selector.select();
      if (selected == 0) {
        continue;
      }

      Set<SelectionKey> selectedKeys = selector.selectedKeys();
      Iterator<SelectionKey> keyIter = selectedKeys.iterator();

      while (keyIter.hasNext()) {
        SelectionKey key = keyIter.next();

        if (key.isAcceptable()) {
          ServerSocketChannel channel =
              (ServerSocketChannel) key.channel();
          SocketChannel clientChannel = channel.accept();
          System.out.println("Incoming connection from "
              + clientChannel.getRemoteAddress());
          clientChannel.configureBlocking(false);
          clientChannel.register(
              selector, SelectionKey.OP_READ);
        }

        if (key.isReadable()) {
          SocketChannel channel =
              (SocketChannel) key.channel();
          channel.read(buffer);
          String request = new String(buffer.array()).trim();
          buffer.clear();
          System.out.println(String.format(
              "Request from %s: %s",
              channel.getRemoteAddress(),
              request));
          String response = requestHandler.handle(request);
          channel.write(ByteBuffer.wrap(response.getBytes()));
        }

        keyIter.remove();
      }
    }
  • 47
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
import java.io.*; import java.net.*; import java.util.*; import java.lang.*; public class Server extends ServerSocket { private static ArrayList User_List = new ArrayList(); private static ArrayList Threader = new ArrayList(); private static LinkedList Message_Array = new LinkedList(); private static int Thread_Counter = 0; private static boolean isClear = true; protected static final int SERVER_PORT = 10000; protected FileOutputStream LOG_FILE = new FileOutputStream( "d:/connect.log", true); public Server() throws FileNotFoundException, IOException { super(SERVER_PORT); // append connection log // Calendar now = Calendar.getInstance(); // String str = "[" + now.getTime().toString() + // "] Accepted a connection"; // byte[] tmp = str.getBytes(); // LOG_FILE.write(tmp); try { Socket socket = accept(); while (true) { new ServerReaderThread(socket); new ServerWriterThread(socket); } } finally { close(); } } public static void main(String[] args) throws IOException { new Server(); } // --- CreateServerThread class ServerReaderThread extends Thread { private Socket client; private BufferedReader in; private PrintWriter out; private String Username; public ServerReaderThread(Socket s) throws IOException { client = s; in = new BufferedReader(new InputStreamReader(client .getInputStream())); out = new PrintWriter(client.getOutputStream(), true); start(); } public void run() { try { int flag = 0; Thread_Counter++; String line = in.readLine(); while (!line.equals("bye")) { out.println(line); line = in.readLine(); } out.println("--- See you, bye! ---"); // System.out.println("--- See you, bye! ---"); client.close(); } catch (IOException e) { } finally { try { client.close(); } catch (IOException e) { } Thread_Counter--; } } } // --- CreateServerThread class ServerWriterThread extends Thread { priva
接入第三方登录是让用户方便快捷地使用已有账号登录你的网站或应用程序,提高用户体验的一种方式。本文将介绍如何使用 PHP 实现微信公众号第三方登录。 1. 获取微信授权 首先,需要获取微信用户的授权。具体步骤如下: 1)引导用户打开微信授权页面: ```php $appid = 'your_appid'; $redirect_uri = urlencode('http://yourdomain.com/callback.php'); $scope = 'snsapi_userinfo'; $url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=$appid&redirect_uri=$redirect_uri&response_type=code&scope=$scope&state=STATE#wechat_redirect"; header("Location: $url"); ``` 其中,`$appid` 是你的微信公众号的 AppID,`$redirect_uri` 是授权后回调的 URL,`$scope` 是授权作用域,可以是 `snsapi_base` 或 `snsapi_userinfo`,`$state` 是自定义参数,用于防止 CSRF 攻击。 2)获取授权码: 用户同意授权后,会重定向到 `$redirect_uri` 指定的 URL,带上授权码 `code` 和 `state` 参数。 ```php $code = $_GET['code']; $state = $_GET['state']; ``` 3)获取 access_token 和 openid: 使用授权码 `code` 获取 `access_token` 和 `openid`。 ```php $access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=$appid&secret=$secret&code=$code&grant_type=authorization_code"; $response = file_get_contents($access_token_url); $result = json_decode($response, true); $access_token = $result['access_token']; $openid = $result['openid']; ``` 其中,`$secret` 是你的微信公众号的 AppSecret。 2. 获取用户信息 获取到 `access_token` 和 `openid` 后,可以使用以下代码获取用户信息: ```php $userinfo_url = "https://api.weixin.qq.com/sns/userinfo?access_token=$access_token&openid=$openid&lang=zh_CN"; $response = file_get_contents($userinfo_url); $userinfo = json_decode($response, true); ``` 其中,`$userinfo` 包含用户的昵称、头像等信息。 3. 将用户信息保存到数据库 最后,将获取到的用户信息保存到数据库中,以便下次使用时快速登录。 ```php // 连接数据库 $con = mysqli_connect('localhost', 'username', 'password', 'database'); mysqli_set_charset($con, "utf8"); // 查询用户是否已存在 $sql = "SELECT * FROM users WHERE openid='$openid'"; $result = mysqli_query($con, $sql); if (mysqli_num_rows($result) == 0) { // 用户不存在,插入新用户信息 $nickname = mysqli_real_escape_string($con, $userinfo['nickname']); $headimgurl = mysqli_real_escape_string($con, $userinfo['headimgurl']); $sql = "INSERT INTO users (openid, nickname, headimgurl) VALUES ('$openid', '$nickname', '$headimgurl')"; mysqli_query($con, $sql); } // 保存用户登录状态 $_SESSION['openid'] = $openid; ``` 以上就是使用 PHP 实现微信公众号第三方登录的步骤。需要注意的是,为了确保安全性,应该对用户输入的数据进行过滤和验证,防止 SQL 注入和 XSS 攻击等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

maplesea7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值